Badge

A small label for status, category or count — pill or square, tinted, outlined or modern, with optional icon, dot, avatar or dismiss button.

Examples

Colours

Eight colour modifiers on the default pill: gray (default), badge_brand, badge_error, badge_warning, badge_success, badge_blue, badge_orange and badge_dark.

<div style="display:flex;flex-wrap:wrap;align-items:center;gap:0.75rem">
  <span class="kompan-badge">Label</span>
  <span class="kompan-badge kompan-badge_brand">Brand</span>
  <span class="kompan-badge kompan-badge_error">Error</span>
  <span class="kompan-badge kompan-badge_warning">Warning</span>
  <span class="kompan-badge kompan-badge_success">Success</span>
  <span class="kompan-badge kompan-badge_blue">Blue</span>
  <span class="kompan-badge kompan-badge_orange">Orange</span>
  <span class="kompan-badge kompan-badge_dark">Dark</span>
</div>

Types

Four shapes: the default pill, badge_outline (border only), badge_square (rounded corners) and badge_square + badge_modern (white surface, neutral text, soft shadow, coloured icon).

<div style="display:flex;flex-wrap:wrap;align-items:center;gap:0.75rem">
  <span class="kompan-badge kompan-badge_brand"><svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9.5v-7M2.5 6 6 2.5 9.5 6"/></svg>Pill</span>
  <span class="kompan-badge kompan-badge_brand kompan-badge_outline"><svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9.5v-7M2.5 6 6 2.5 9.5 6"/></svg>Outline</span>
  <span class="kompan-badge kompan-badge_brand kompan-badge_square"><svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9.5v-7M2.5 6 6 2.5 9.5 6"/></svg>Square</span>
  <span class="kompan-badge kompan-badge_brand kompan-badge_square kompan-badge_modern"><svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9.5v-7M2.5 6 6 2.5 9.5 6"/></svg>Modern</span>
</div>

Sizes

badge_sm (default, 22px), badge_md (24px) and badge_lg (28px). md and lg share the sm text size; lg adds vertical padding.

<div style="display:flex;flex-wrap:wrap;align-items:center;gap:0.75rem">
  <span class="kompan-badge kompan-badge_sm kompan-badge_success"><svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9.5v-7M2.5 6 6 2.5 9.5 6"/></svg>Small</span>
  <span class="kompan-badge kompan-badge_md kompan-badge_success"><svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9.5v-7M2.5 6 6 2.5 9.5 6"/></svg>Medium</span>
  <span class="kompan-badge kompan-badge_lg kompan-badge_success"><svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9.5v-7M2.5 6 6 2.5 9.5 6"/></svg>Large</span>
</div>

Icons and dot

Put a 12px svg.badge__icon before or after the label; a leading span.badge__dot shows a status disc. The outer padding tightens automatically on the icon side.

<div style="display:flex;flex-wrap:wrap;align-items:center;gap:0.75rem">
  <span class="kompan-badge kompan-badge_blue"><svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9.5v-7M2.5 6 6 2.5 9.5 6"/></svg>Leading</span>
  <span class="kompan-badge kompan-badge_blue">Trailing<svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 6h7M6 2.5 9.5 6 6 9.5"/></svg></span>
  <span class="kompan-badge kompan-badge_success"><span class="kompan-badge__dot" aria-hidden="true"></span>Active</span>
  <span class="kompan-badge"><span class="kompan-badge__dot" aria-hidden="true"></span>Offline</span>
</div>

Avatar and flag

A 16px round image in badge__avatar — a person, or a country flag for locale badges.

<div style="display:flex;flex-wrap:wrap;align-items:center;gap:0.75rem">
  <span class="kompan-badge kompan-badge_lg"><span class="kompan-badge__avatar"><img src="https://images.unsplash.com/photo-1600603406200-5b2a104684ac?w=64&h=64&fit=crop&crop=faces&q=80" alt="" /></span>Mette Hansen</span>
  <span class="kompan-badge kompan-badge_lg kompan-badge_brand"><span class="kompan-badge__avatar"><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="8" fill="#d80027"/><path d="M6.5 0.1v6.4H0.1a8 8 0 0 0 0 3h6.4v6.4a8 8 0 0 0 3 0V9.5h6.4a8 8 0 0 0 0-3H9.5V0.1a8 8 0 0 0-3 0Z" fill="#f0f0f0"/></svg></span>Denmark</span>
</div>

Dismissible

A trailing button.badge__close removes the badge. Give it an aria-label that names what is removed.

<div style="display:flex;flex-wrap:wrap;align-items:center;gap:0.75rem">
  <span class="kompan-badge kompan-badge_md">Filter: Playgrounds<button class="kompan-badge__close" type="button" aria-label="Remove filter Playgrounds"><svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 3 3 9M3 3l6 6"/></svg></button></span>
  <span class="kompan-badge kompan-badge_md kompan-badge_brand kompan-badge_square">Brand<button class="kompan-badge__close" type="button" aria-label="Remove Brand"><svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 3 3 9M3 3l6 6"/></svg></button></span>
</div>

Icon only

badge_icon-only pads equally on all sides. The icon carries no text, so add an accessible name.

<div style="display:flex;flex-wrap:wrap;align-items:center;gap:0.75rem">
  <span class="kompan-badge kompan-badge_icon-only kompan-badge_blue" role="img" aria-label="Add"><svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2.5v7M2.5 6h7"/></svg></span>
  <span class="kompan-badge kompan-badge_md kompan-badge_icon-only kompan-badge_blue" role="img" aria-label="Add"><svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2.5v7M2.5 6h7"/></svg></span>
  <span class="kompan-badge kompan-badge_lg kompan-badge_icon-only kompan-badge_blue" role="img" aria-label="Add"><svg class="kompan-badge__icon" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2.5v7M2.5 6h7"/></svg></span>
</div>

Accessibility

Badges are text, not controls

A badge is a plain span with visible text; it needs no role. Do not make the whole badge clickable — put actions in a real button (see the dismissible pattern) so keyboard users can reach them.

Colour must not carry the only meaning

The colour modifiers and badge__dot convey state through colour alone. Make sure the label itself says what the state is ("Active", "Error"), or add visually-hidden text (class sr-only) next to the dot.

Decorative icons and avatars

badge__icon, badge__dot and the flag/portrait in badge__avatar are decorative when the label already names the thing. Mark svgs aria-hidden="true" and use alt="" on the image. Only the icon-only badge needs its own name: give it role="img" and an aria-label.

Dismiss button needs a name

button.badge__close shows only an × icon. Set aria-label="Remove {label}" so the action is announced with its target, and keep the visible focus ring.

Contrast

Text uses the 700 step on the 50 tint (warning: 700 on 100), which meets AA for the 12–14px medium-weight label. Icons at the 500 step on the tint are graphics — keep them decorative, or lift them to the 600 step (outline/square types) when they carry meaning.

Stylesheet

Shipped as @wah-kompan/kompan-ui/components/badge.css. Every value is a token reference, so it themes with the rest of the system rather than being restyled.

css
.kompan-badge {
  /* Size ramp — sm is the Figma default */
  --kompan-badge-py: var(--spacing-xxs);
  --kompan-badge-px: var(--spacing-md);
  --kompan-badge-px-icon: var(--spacing-sm);
  --kompan-badge-px-media: 0.1875rem;
  --kompan-badge-gap: var(--spacing-xs);
  --kompan-badge-radius: var(--radius-full);
  --kompan-badge-square-radius: var(--radius-sm);
  /* Colour scale — gray is the Figma default */
  --kompan-badge-tint: var(--color-utility-gray-50);
  --kompan-badge-tint-border: var(--color-utility-gray-200);
  --kompan-badge-icon: var(--color-utility-gray-500);
  --kompan-badge-strong: var(--color-utility-gray-600);
  --kompan-badge-text: var(--color-utility-gray-700);
  --kompan-badge-modern-icon: var(--color-fg-quaternary);
  /* Resolved per type (Pill color is the Figma default) */
  --kompan-badge-bg: var(--kompan-badge-tint);
  --kompan-badge-border: var(--kompan-badge-tint-border);
  --kompan-badge-fg: var(--kompan-badge-text);
  --kompan-badge-icon-fg: var(--kompan-badge-icon);

  display: inline-flex;
  align-items: center;
  gap: var(--kompan-badge-gap);
  padding: var(--kompan-badge-py) var(--kompan-badge-px);
  border-radius: var(--kompan-badge-radius);
  background: var(--kompan-badge-bg);
  box-shadow: inset 0 0 0 var(--stroke-sm) var(--kompan-badge-border);
  color: var(--kompan-badge-fg);
  font-family: var(--type-font-family-body);
  font-size: var(--type-font-size-xs);
  font-weight: var(--type-font-weight-medium);
  line-height: var(--type-line-height-xs);
  white-space: nowrap;
  vertical-align: middle;
}

/* Sizes */
.kompan-badge_sm {
  --kompan-badge-py: var(--spacing-xxs);
  --kompan-badge-px: var(--spacing-md);
  --kompan-badge-px-icon: var(--spacing-sm);
  --kompan-badge-px-media: 0.1875rem;
  --kompan-badge-square-radius: var(--radius-sm);
  font-size: var(--type-font-size-xs);
  line-height: var(--type-line-height-xs);
}
.kompan-badge_md {
  --kompan-badge-py: var(--spacing-xxs);
  --kompan-badge-px: 0.625rem;
  --kompan-badge-px-icon: var(--spacing-md);
  --kompan-badge-px-media: var(--spacing-xs);
  --kompan-badge-square-radius: var(--radius-sm);
  font-size: var(--type-font-size-sm);
  line-height: var(--type-line-height-sm);
}
.kompan-badge_lg {
  --kompan-badge-py: var(--spacing-xs);
  --kompan-badge-px: var(--spacing-lg);
  --kompan-badge-px-icon: 0.625rem;
  --kompan-badge-px-media: var(--spacing-sm);
  --kompan-badge-gap: var(--spacing-xs);
  --kompan-badge-square-radius: var(--radius-md);
  font-size: var(--type-font-size-sm);
  line-height: var(--type-line-height-sm);
}

/* Types */
.kompan-badge_outline {
  --kompan-badge-bg: transparent;
  --kompan-badge-border: var(--kompan-badge-strong);
  --kompan-badge-icon-fg: var(--kompan-badge-strong);
}
.kompan-badge_square {
  --kompan-badge-radius: var(--kompan-badge-square-radius);
  --kompan-badge-icon-fg: var(--kompan-badge-strong);
}
.kompan-badge_square { --kompan-badge-px: var(--spacing-sm); --kompan-badge-px-icon: var(--spacing-xs); }
.kompan-badge_md.kompan-badge_square { --kompan-badge-px: var(--spacing-md); --kompan-badge-px-icon: var(--spacing-sm); }
.kompan-badge_lg.kompan-badge_square { --kompan-badge-px: 0.625rem; --kompan-badge-px-icon: var(--spacing-md); }
.kompan-badge_modern {
  --kompan-badge-bg: var(--color-bg-primary);
  --kompan-badge-border: var(--color-border-primary);
  --kompan-badge-fg: var(--color-text-secondary);
  --kompan-badge-icon-fg: var(--kompan-badge-modern-icon);
  box-shadow:
    inset 0 0 0 var(--stroke-sm) var(--kompan-badge-border),
    0 1px 2px 0 var(--color-utility-alpha-black-5);
}

/* Colours */
.kompan-badge_brand {
  --kompan-badge-tint: var(--color-utility-brand-50);
  --kompan-badge-tint-border: var(--color-utility-brand-200);
  --kompan-badge-icon: var(--color-utility-brand-500);
  --kompan-badge-strong: var(--color-utility-brand-600);
  --kompan-badge-text: var(--color-utility-brand-700);
  --kompan-badge-modern-icon: var(--color-utility-brand-600);
}
.kompan-badge_error {
  --kompan-badge-tint: var(--color-utility-error-50);
  --kompan-badge-tint-border: var(--color-utility-error-200);
  --kompan-badge-icon: var(--color-utility-error-500);
  --kompan-badge-strong: var(--color-utility-error-600);
  --kompan-badge-text: var(--color-utility-error-700);
  --kompan-badge-modern-icon: var(--color-utility-error-600);
}
.kompan-badge_warning {
  --kompan-badge-tint: var(--color-utility-warning-100);
  --kompan-badge-tint-border: var(--color-utility-warning-600);
  --kompan-badge-icon: var(--color-utility-warning-600);
  --kompan-badge-strong: var(--color-utility-warning-600);
  --kompan-badge-text: var(--color-utility-warning-700);
  --kompan-badge-modern-icon: var(--color-utility-warning-600);
}
.kompan-badge_success {
  --kompan-badge-tint: var(--color-utility-success-50);
  --kompan-badge-tint-border: var(--color-utility-success-200);
  --kompan-badge-icon: var(--color-utility-success-500);
  --kompan-badge-strong: var(--color-utility-success-600);
  --kompan-badge-text: var(--color-utility-success-700);
  --kompan-badge-modern-icon: var(--color-utility-success-600);
}
.kompan-badge_blue {
  --kompan-badge-tint: var(--color-utility-blue-50);
  --kompan-badge-tint-border: var(--color-utility-blue-200);
  --kompan-badge-icon: var(--color-utility-blue-500);
  --kompan-badge-strong: var(--color-utility-blue-600);
  --kompan-badge-text: var(--color-utility-blue-700);
  --kompan-badge-modern-icon: var(--color-utility-blue-500);
}
.kompan-badge_orange {
  --kompan-badge-tint: var(--color-utility-orange-50);
  --kompan-badge-tint-border: var(--color-utility-orange-200);
  --kompan-badge-icon: var(--color-utility-orange-500);
  --kompan-badge-strong: var(--color-utility-orange-600);
  --kompan-badge-text: var(--color-utility-orange-700);
  --kompan-badge-modern-icon: var(--color-utility-orange-500);
}
/* Figma: blue and orange icons stay at the 500 step in every type */
.kompan-badge_blue.kompan-badge_outline, .kompan-badge_blue.kompan-badge_square,
.kompan-badge_orange.kompan-badge_outline, .kompan-badge_orange.kompan-badge_square {
  --kompan-badge-icon-fg: var(--kompan-badge-icon);
}
.kompan-badge_dark {
  --kompan-badge-tint: var(--color-utility-alpha-black-100);
  --kompan-badge-tint-border: var(--color-utility-gray-800);
  --kompan-badge-icon: var(--color-utility-alpha-white-100);
  --kompan-badge-strong: var(--color-utility-gray-800);
  --kompan-badge-text: var(--color-utility-alpha-white-100);
  --kompan-badge-modern-icon: var(--color-utility-alpha-white-100);
}

/* Slots — the outer edge next to media tightens to the icon padding */
.kompan-badge__icon,
.kompan-badge__dot,
.kompan-badge__avatar,
.kompan-badge__close {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.kompan-badge__icon { width: 0.75rem; height: 0.75rem; color: var(--kompan-badge-icon-fg); }
.kompan-badge__icon:first-child { margin-left: calc(var(--kompan-badge-px-icon) - var(--kompan-badge-px)); }
.kompan-badge__icon:last-child  { margin-right: calc(var(--kompan-badge-px-icon) - var(--kompan-badge-px)); }

.kompan-badge__dot {
  width: 0.5rem;
  height: 0.5rem;
  margin-left: calc(var(--kompan-badge-px-icon) - var(--kompan-badge-px));
}
.kompan-badge__dot::before {
  content: '';
  width: 0.375rem;
  height: 0.375rem;
  border-radius: var(--radius-full);
  background: var(--kompan-badge-icon);
}

.kompan-badge__avatar {
  width: 1rem;
  height: 1rem;
  margin-left: calc(var(--kompan-badge-px-media) - var(--kompan-badge-px));
  border-radius: var(--radius-full);
  overflow: hidden;
}
.kompan-badge__avatar img, .kompan-badge__avatar svg { width: 100%; height: 100%; object-fit: cover; }

.kompan-badge__close {
  width: 1rem;
  height: 1rem;
  margin-right: calc(var(--kompan-badge-px-media) - var(--kompan-badge-px));
  padding: var(--spacing-xxs);
  border: 0;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--kompan-badge-icon-fg);
  cursor: pointer;
}
.kompan-badge__close svg { width: 100%; height: 100%; }
.kompan-badge__close:hover { background: var(--kompan-badge-tint-border); }
.kompan-badge__close:focus-visible { outline: var(--stroke-md) solid var(--kompan-badge-strong); outline-offset: 1px; }
.kompan-badge_square .kompan-badge__close { border-radius: var(--radius-xs); }

/* Icon only — equal padding on every side */
.kompan-badge_icon-only { padding: 0.3125rem; }
.kompan-badge_md.kompan-badge_icon-only { padding: var(--spacing-sm); }
.kompan-badge_lg.kompan-badge_icon-only { padding: var(--spacing-md); }
.kompan-badge_icon-only .kompan-badge__icon { margin: 0; }