Skip to content
Star on GitHub
Forms and inputsBadge
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Badge

Small labels for plans, environments and status, with a dot, an icon or a remove button. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add badge
Tones
DraftBetaLiveDegradedFailed
Colours
SearchReasoningDataBillingUrgentReviewDesignOps
Sizes
ProPro
Dot or icon
RunningIdlePrivateAgent
Removable
ProductionStagingPreview

01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add badge

React

$npx designme add badge --react

The CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.

02

About

What it is for and what it does.

Badge is a small label for a plan, an environment or a status: "Pro", "Production", "Failed". It comes in thirteen tones, neutral, the accent, success, warning and danger, plus eight tag colours, each drawn as text on a tint of its own colour. It has two sizes, an optional dot or icon in front of the label, and an optional remove button after it.

  • When a name needs a short qualifier beside it, like a plan next to an organisation or an environment next to a project.
  • When a list mixes states that people scan for, like Live, Degraded and Failed runs.
  • When labels can be taken off one at a time, like the environments a view is filtered to.
  • Not for actions: use a button, or a chip in Multi-select or Filter menu, for something people press. A badge that does something, a removable one or one written as a link or a button, shows a light hover so it reads as one.
03

Usage

Import it and drive it from your own state.

Import

import { Badge } from '@/components/designme/badge/Badge';

Example

The demo above as a React component, 43 lines you can copy.

'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useRef, useState, type ReactNode } from 'react';
import { Badge, type BadgeTone } from './Badge';
import { useStory } from '../lib/useScript';

const ENVS: { label: string; tone: BadgeTone }[] = [{ label: 'Production', tone: 'warning' }, { label: 'Staging', tone: 'blue' }, { label: 'Preview', tone: 'violet' }];

/* ---------- Badge: every tone, both sizes, a dot or an icon, and removable ones; Staging is taken off, then put back ---------- */
export default function BadgeDemo() {
  const ref = useRef<HTMLDivElement>(null);
  // the removable badges still showing, and the ones taken off, the latest last
  const [shown, setShown] = useState(ENVS.map(e => e.label));
  const [gone, setGone] = useState<string[]>([]);
  const [said, setSaid] = useState('');
  const remove = (label: string) => { setShown(s => s.filter(x => x !== label)); setGone(g => [...g, label]); setSaid(`Removed ${label}`); };
  const undo = () => {
    const last = gone[gone.length - 1]; if (!last) return;
    setGone(g => g.slice(0, -1)); setShown(s => ENVS.map(e => e.label).filter(x => x === last || s.includes(x))); setSaid(`${last} is back`);
  };
  const undoRef = useRef(undo); undoRef.current = undo;
  // the story stops at the visitor's first click or key, so it never removes a badge they meant to keep
  useStory(ref, async ({ wait }) => {
    await wait(2000); remove('Staging');
    await wait(2000); undoRef.current();
  });
  const row = (label: string, children: ReactNode) => <div className="dm-bdx-row"><span className="dm-bdx-l dm-muted">{label}</span><div className="dm-bdx-set">{children}</div></div>;
  return (
    <div ref={ref} data-dm="" className="dm-bdx">
      {row('Tones', <><Badge>Draft</Badge><Badge tone="accent">Beta</Badge><Badge tone="success">Live</Badge><Badge tone="warning">Degraded</Badge><Badge tone="danger">Failed</Badge></>)}
      {row('Colours', <><Badge tone="blue">Search</Badge><Badge tone="violet">Reasoning</Badge><Badge tone="green">Data</Badge><Badge tone="orange">Billing</Badge>
        <Badge tone="red">Urgent</Badge><Badge tone="yellow">Review</Badge><Badge tone="pink">Design</Badge><Badge tone="teal">Ops</Badge></>)}
      {row('Sizes', <><Badge size="sm" tone="accent">Pro</Badge><Badge tone="accent">Pro</Badge></>)}
      {row('Dot or icon', <><Badge tone="success" dot>Running</Badge><Badge dot>Idle</Badge><Badge icon="lock">Private</Badge><Badge tone="accent" icon="spark">Agent</Badge></>)}
      {row('Removable', <>
        {ENVS.filter(e => shown.includes(e.label)).map(e => <Badge key={e.label} tone={e.tone} onRemove={() => remove(e.label)}>{e.label}</Badge>)}
        {gone.length > 0 && <button type="button" className="dm-pill dm-ghost dm-sm dm-bdx-undo" onClick={undo}>Undo</button>}
      </>)}
      <p className="dm-bdx-live" aria-live="polite">{said}</p>
    </div>
  );
}
04

Props

Everything Badge takes.

PropTypeWhat it does
childrenrequiredReactNodeThe label, like "Pro", "Production" or "Failed"
toneBadgeToneIts colour, as text on its own tint. Neutral by default
size'sm' | 'md''sm' is 20px tall, 'md' 24px (the default)
dotbooleanA dot in the badge's colour before the label, for a status like Running
iconIconName | ReactNodeAn icon before the label: a name from the icon set, or your own element
onRemove() => voidAdds a remove button after the label. Take the badge out of your list in it
removeLabelstringThe remove button's name for screen readers. Defaults to "Remove" and the label, when the label is text
classNamestring
05

Code

The full source, as the CLI installs it.

Badge.tsxReact · 45 lines
'use client';
import { isValidElement, type ReactNode } from 'react';
import { cx } from '../lib/cx';
import { Icon, type IconName } from '../lib/icons';
import '../designme.css';
import './badge.css';

/** A badge's colour: neutral grey, the accent's tint, the three states, or one of the eight tag colours */
export type BadgeTone = 'neutral' | 'accent' | 'success' | 'warning' | 'danger' | 'blue' | 'violet' | 'green' | 'orange' | 'red' | 'yellow' | 'pink' | 'teal';

export interface BadgeProps {
  /** The label, like "Pro", "Production" or "Failed" */
  children: ReactNode;
  /** Its colour, as text on its own tint. Neutral by default */
  tone?: BadgeTone;
  /** 'sm' is 20px tall, 'md' 24px (the default) */
  size?: 'sm' | 'md';
  /** A dot in the badge's colour before the label, for a status like Running */
  dot?: boolean;
  /** An icon before the label: a name from the icon set, or your own element */
  icon?: IconName | ReactNode;
  /** Adds a remove button after the label. Take the badge out of your list in it */
  onRemove?: () => void;
  /** The remove button's name for screen readers. Defaults to "Remove" and the label, when the label is text */
  removeLabel?: string;
  className?: string;
}

/** A small label for a plan, an environment or a status, with an optional dot or icon and an optional remove button. */
export function Badge({ children, tone = 'neutral', size = 'md', dot = false, icon, onRemove, removeLabel, className }: BadgeProps) {
  const text = typeof children === 'string' || typeof children === 'number' ? String(children) : '';
  return (
    <span data-dm="" className={cx('dm-bdg', size === 'sm' && 'dm-sm', className)} data-tone={tone === 'neutral' ? undefined : tone}>
      {dot && <span className="dm-bdg-dot" aria-hidden="true" />}
      {typeof icon === 'string' ? <Icon name={icon as IconName} size="xs" /> : isValidElement(icon) ? icon : null}
      <span className="dm-t">{children}</span>
      {onRemove && (
        <button type="button" className="dm-bdg-x" aria-label={removeLabel ?? (text ? `Remove ${text}` : 'Remove')} onClick={onRemove}>
          <Icon name="x" size="xs" />
        </button>
      )}
    </span>
  );
}
badge.cssReact · 51 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Badge ---------- */
/* a small label for a plan, an environment or a status. data-tone picks its colour (none is neutral): the accent's tint,
   success, warning and danger, or one of the eight tag colours, each as text on its own tint. Two sizes (.sm), an optional
   dot or icon in front, and an optional remove button after the label. Soft draws it as a pill; Blueprint squares it off,
   sets it in capitals and gives it a hairline in its own colour */
.dm-bdg{--dm-bdg-bg:var(--dm-chip);--dm-bdg-fg:var(--dm-fg2);display:inline-flex;align-items:center;gap:var(--dm-s-1);max-width:100%;height:24px;padding:0 var(--dm-s-2);border-radius:var(--dm-r-pill);background:var(--dm-bdg-bg);color:var(--dm-bdg-fg);font-size:12.5px;font-weight:500;line-height:1;white-space:nowrap;vertical-align:middle}
.dm-bdg.dm-sm{height:20px;font-size:11.5px}
.dm-bdg>.dm-t{min-width:0;overflow:hidden;text-overflow:ellipsis}
.dm-bdg>.dm-i{flex:none}
.dm-bdg-dot{position:relative;width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
/* a live badge (.live: running, streaming, on air) pulses its dot: a soft ring in its own colour grows and fades, on a loop.
   Only transform and opacity move, so it stays on the compositor; with reduced motion the dot stays still */
.dm-bdg.dm-live .dm-bdg-dot::after{content:"";position:absolute;inset:0;border-radius:inherit;background:currentColor;opacity:0;animation:dm-bdgpulse 1.8s cubic-bezier(.22,1,.36,1) infinite}
@keyframes dm-bdgpulse{0%{opacity:.5;transform:scale(1)}100%{opacity:0;transform:scale(2.8)}}
@media (prefers-reduced-motion:reduce){.dm-bdg.dm-live .dm-bdg-dot::after{animation:none}}
/* a badge you can act on, a removable one or one that is a button or a link, takes a light hover: a little more of its own
   colour in its tint. Plain labels keep still, so they never look clickable */
.dm-bdg:is(:has(>.dm-bdg-x),button,a[href]){transition:background var(--dm-t-fast)}
.dm-bdg:is(button,a[href]){cursor:pointer;text-decoration:none}
@media (hover:hover) and (pointer:fine){.dm-bdg:is(:has(>.dm-bdg-x),button,a[href]):hover{background:color-mix(in srgb,var(--dm-bdg-fg) 7%,var(--dm-bdg-bg))}}
/* on the Badge card every badge answers the pointer: a hand, a light hover, and no text selection */
.dm-bdx .dm-bdg{cursor:pointer;user-select:none;-webkit-user-select:none;transition:background var(--dm-t-fast)}
@media (hover:hover) and (pointer:fine){.dm-bdx .dm-bdg:hover{background:color-mix(in srgb,var(--dm-bdg-fg) 7%,var(--dm-bdg-bg))}}
.dm-bdg[data-tone="accent"]{--dm-bdg-bg:var(--dm-accent-bg);--dm-bdg-fg:var(--dm-accent-fg)}
.dm-bdg[data-tone="success"]{--dm-bdg-bg:var(--dm-ok-bg);--dm-bdg-fg:var(--dm-ok)}.dm-bdg[data-tone="warning"]{--dm-bdg-bg:var(--dm-warn-bg);--dm-bdg-fg:var(--dm-warn)}.dm-bdg[data-tone="danger"]{--dm-bdg-bg:var(--dm-bad-bg);--dm-bdg-fg:var(--dm-bad)}
.dm-bdg[data-tone="blue"]{--dm-bdg-bg:var(--dm-pb-bg);--dm-bdg-fg:var(--dm-pb)}.dm-bdg[data-tone="violet"]{--dm-bdg-bg:var(--dm-pl-bg);--dm-bdg-fg:var(--dm-pl)}.dm-bdg[data-tone="green"]{--dm-bdg-bg:var(--dm-pm-bg);--dm-bdg-fg:var(--dm-pm)}.dm-bdg[data-tone="orange"]{--dm-bdg-bg:var(--dm-pp-bg);--dm-bdg-fg:var(--dm-pp)}
.dm-bdg[data-tone="red"]{--dm-bdg-bg:var(--dm-pr-bg);--dm-bdg-fg:var(--dm-pr)}.dm-bdg[data-tone="yellow"]{--dm-bdg-bg:var(--dm-py-bg);--dm-bdg-fg:var(--dm-py)}.dm-bdg[data-tone="pink"]{--dm-bdg-bg:var(--dm-pk-bg);--dm-bdg-fg:var(--dm-pk)}.dm-bdg[data-tone="teal"]{--dm-bdg-bg:var(--dm-pt-bg);--dm-bdg-fg:var(--dm-pt)}
/* the remove button sits inside the badge's end; its hover is a little more of the badge's own colour */
.dm-bdg-x{display:grid;place-items:center;width:16px;height:16px;margin-right:calc(var(--dm-s-1) * -1);border-radius:50%;color:inherit;flex:none;transition:background var(--dm-t-fast)}
@media (hover:hover) and (pointer:fine){.dm-bdg-x:hover{background:color-mix(in srgb,currentColor 16%,transparent)}}
.dm-bdg-x:focus-visible{outline-offset:1px;border-radius:50%}
.dm-bdg.dm-sm .dm-bdg-x{width:14px;height:14px}
/* on a hovered menu row's tint (a step lighter in dark mode) the label leans a little toward the text colour, so it keeps APCA 60 */
.dm-mi.dm-hl .dm-bdg{color:color-mix(in srgb,var(--dm-bdg-fg) 85%,var(--dm-fg))}
@media (hover:hover) and (pointer:fine){.dm-mi:hover .dm-bdg{color:color-mix(in srgb,var(--dm-bdg-fg) 85%,var(--dm-fg))}}
.dm-bdg.dm-go{animation:dm-bdgout .22s var(--dm-ease) forwards}
@keyframes dm-bdgout{to{opacity:0;transform:scale(.85)}}
:root:not([data-style="soft"]) .dm-bdg:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-bdg-fg) 14%,transparent);font-size:11px;letter-spacing:.03em;text-transform:uppercase}
:root:not([data-style="soft"]) .dm-bdg.dm-sm:not(:where([data-style="soft"],[data-style="soft"] *)){font-size:10.5px}
/* Blueprint's lime tint is faint on white, so an accent badge's edge is the accent's mark */
:root:not([data-style="soft"]) .dm-bdg[data-tone="accent"]:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-accent-mark) 50%,transparent)}
/* the card: each kind of badge in a row, named on the left */
.dm-bdx{width:100%;display:flex;flex-direction:column;gap:var(--dm-s-4);container:bdx/inline-size}
.dm-bdx-row{display:flex;align-items:center;gap:var(--dm-s-2)}
.dm-bdx-l{width:76px;flex:none;font-size:12px}
.dm-bdx-set{flex:1;min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:var(--dm-s-2)}
.dm-pill.dm-bdx-undo{height:24px}
.dm-bdx-live{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@container bdx (max-width:360px){.dm-bdx-row{flex-direction:column;align-items:flex-start;gap:var(--dm-s-2)}.dm-bdx-l{width:auto}.dm-bdx-set{width:100%}}
badge.htmlHTML · 13 lines
<div class="card" data-c="badge">
  <div class="stage-in">
    <div class="bdx">
      <div class="bdx-row"><span class="bdx-l muted">Tones</span><div class="bdx-set"><span class="bdg">Draft</span><span class="bdg" data-tone="accent">Beta</span><span class="bdg" data-tone="success">Live</span><span class="bdg" data-tone="warning">Degraded</span><span class="bdg" data-tone="danger">Failed</span></div></div>
      <div class="bdx-row"><span class="bdx-l muted">Colours</span><div class="bdx-set"><span class="bdg" data-tone="blue">Search</span><span class="bdg" data-tone="violet">Reasoning</span><span class="bdg" data-tone="green">Data</span><span class="bdg" data-tone="orange">Billing</span><span class="bdg" data-tone="red">Urgent</span><span class="bdg" data-tone="yellow">Review</span><span class="bdg" data-tone="pink">Design</span><span class="bdg" data-tone="teal">Ops</span></div></div>
      <div class="bdx-row"><span class="bdx-l muted">Sizes</span><div class="bdx-set"><span class="bdg sm" data-tone="accent">Pro</span><span class="bdg" data-tone="accent">Pro</span></div></div>
      <div class="bdx-row"><span class="bdx-l muted">Dot or icon</span><div class="bdx-set"><span class="bdg live" data-tone="success"><span class="bdg-dot"></span>Running</span><span class="bdg"><span class="bdg-dot"></span>Idle</span><span class="bdg"><svg class="i xs"><use href="#i-lock"/></svg>Private</span><span class="bdg" data-tone="accent"><svg class="i xs"><use href="#i-spark"/></svg>Agent</span></div></div>
      <div class="bdx-row"><span class="bdx-l muted">Removable</span><div class="bdx-set bdx-rm"><span class="bdg" data-tone="warning"><span class="t">Production</span><button class="bdg-x" aria-label="Remove Production"><svg class="i xs"><use href="#i-x"/></svg></button></span><span class="bdg" data-tone="blue"><span class="t">Staging</span><button class="bdg-x" aria-label="Remove Staging"><svg class="i xs"><use href="#i-x"/></svg></button></span><span class="bdg" data-tone="violet"><span class="t">Preview</span><button class="bdg-x" aria-label="Remove Preview"><svg class="i xs"><use href="#i-x"/></svg></button></span><button class="pill ghost sm bdx-undo" hidden>Undo</button></div></div>
      <p class="bdx-live" aria-live="polite"></p>
    </div>
  </div>
</div>
badge.cssHTML · 50 lines
/* ---------- Badge ---------- */
/* a small label for a plan, an environment or a status. data-tone picks its colour (none is neutral): the accent's tint,
   success, warning and danger, or one of the eight tag colours, each as text on its own tint. Two sizes (.sm), an optional
   dot or icon in front, and an optional remove button after the label. Soft draws it as a pill; Blueprint squares it off,
   sets it in capitals and gives it a hairline in its own colour */
.bdg{--bdg-bg:var(--chip);--bdg-fg:var(--fg2);display:inline-flex;align-items:center;gap:var(--s-1);max-width:100%;height:24px;padding:0 var(--s-2);border-radius:var(--r-pill);background:var(--bdg-bg);color:var(--bdg-fg);font-size:12.5px;font-weight:500;line-height:1;white-space:nowrap;vertical-align:middle}
.bdg.sm{height:20px;font-size:11.5px}
.bdg>.t{min-width:0;overflow:hidden;text-overflow:ellipsis}
.bdg>.i{flex:none}
.bdg-dot{position:relative;width:6px;height:6px;border-radius:50%;background:currentColor;flex:none}
/* a live badge (.live: running, streaming, on air) pulses its dot: a soft ring in its own colour grows and fades, on a loop.
   Only transform and opacity move, so it stays on the compositor; with reduced motion the dot stays still */
.bdg.live .bdg-dot::after{content:"";position:absolute;inset:0;border-radius:inherit;background:currentColor;opacity:0;animation:bdgpulse 1.8s cubic-bezier(.22,1,.36,1) infinite}
@keyframes bdgpulse{0%{opacity:.5;transform:scale(1)}100%{opacity:0;transform:scale(2.8)}}
@media (prefers-reduced-motion:reduce){.bdg.live .bdg-dot::after{animation:none}}
/* a badge you can act on, a removable one or one that is a button or a link, takes a light hover: a little more of its own
   colour in its tint. Plain labels keep still, so they never look clickable */
.bdg:is(:has(>.bdg-x),button,a[href]){transition:background var(--t-fast)}
.bdg:is(button,a[href]){cursor:pointer;text-decoration:none}
@media (hover:hover) and (pointer:fine){.bdg:is(:has(>.bdg-x),button,a[href]):hover{background:color-mix(in srgb,var(--bdg-fg) 7%,var(--bdg-bg))}}
/* on the Badge card every badge answers the pointer: a hand, a light hover, and no text selection */
.bdx .bdg{cursor:pointer;user-select:none;-webkit-user-select:none;transition:background var(--t-fast)}
@media (hover:hover) and (pointer:fine){.bdx .bdg:hover{background:color-mix(in srgb,var(--bdg-fg) 7%,var(--bdg-bg))}}
.bdg[data-tone="accent"]{--bdg-bg:var(--accent-bg);--bdg-fg:var(--accent-fg)}
.bdg[data-tone="success"]{--bdg-bg:var(--ok-bg);--bdg-fg:var(--ok)}.bdg[data-tone="warning"]{--bdg-bg:var(--warn-bg);--bdg-fg:var(--warn)}.bdg[data-tone="danger"]{--bdg-bg:var(--bad-bg);--bdg-fg:var(--bad)}
.bdg[data-tone="blue"]{--bdg-bg:var(--pb-bg);--bdg-fg:var(--pb)}.bdg[data-tone="violet"]{--bdg-bg:var(--pl-bg);--bdg-fg:var(--pl)}.bdg[data-tone="green"]{--bdg-bg:var(--pm-bg);--bdg-fg:var(--pm)}.bdg[data-tone="orange"]{--bdg-bg:var(--pp-bg);--bdg-fg:var(--pp)}
.bdg[data-tone="red"]{--bdg-bg:var(--pr-bg);--bdg-fg:var(--pr)}.bdg[data-tone="yellow"]{--bdg-bg:var(--py-bg);--bdg-fg:var(--py)}.bdg[data-tone="pink"]{--bdg-bg:var(--pk-bg);--bdg-fg:var(--pk)}.bdg[data-tone="teal"]{--bdg-bg:var(--pt-bg);--bdg-fg:var(--pt)}
/* the remove button sits inside the badge's end; its hover is a little more of the badge's own colour */
.bdg-x{display:grid;place-items:center;width:16px;height:16px;margin-right:calc(var(--s-1) * -1);border-radius:50%;color:inherit;flex:none;transition:background var(--t-fast)}
@media (hover:hover) and (pointer:fine){.bdg-x:hover{background:color-mix(in srgb,currentColor 16%,transparent)}}
.bdg-x:focus-visible{outline-offset:1px;border-radius:50%}
.bdg.sm .bdg-x{width:14px;height:14px}
/* on a hovered menu row's tint (a step lighter in dark mode) the label leans a little toward the text colour, so it keeps APCA 60 */
.mi.hl .bdg{color:color-mix(in srgb,var(--bdg-fg) 85%,var(--fg))}
@media (hover:hover) and (pointer:fine){.mi:hover .bdg{color:color-mix(in srgb,var(--bdg-fg) 85%,var(--fg))}}
.bdg.go{animation:bdgout .22s var(--ease) forwards}
@keyframes bdgout{to{opacity:0;transform:scale(.85)}}
:root:not([data-style="soft"]) .bdg:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--bdg-fg) 14%,transparent);font-size:11px;letter-spacing:.03em;text-transform:uppercase}
:root:not([data-style="soft"]) .bdg.sm:not(:where([data-style="soft"],[data-style="soft"] *)){font-size:10.5px}
/* Blueprint's lime tint is faint on white, so an accent badge's edge is the accent's mark */
:root:not([data-style="soft"]) .bdg[data-tone="accent"]:not(:where([data-style="soft"],[data-style="soft"] *)){box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent-mark) 50%,transparent)}
/* the card: each kind of badge in a row, named on the left */
.bdx{width:100%;display:flex;flex-direction:column;gap:var(--s-4);container:bdx/inline-size}
.bdx-row{display:flex;align-items:center;gap:var(--s-2)}
.bdx-l{width:76px;flex:none;font-size:12px}
.bdx-set{flex:1;min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2)}
.pill.bdx-undo{height:24px}
.bdx-live{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@container bdx (max-width:360px){.bdx-row{flex-direction:column;align-items:flex-start;gap:var(--s-2)}.bdx-l{width:auto}.bdx-set{width:100%}}
badge.jsHTML · 40 lines
DM.def('badge', (r, c) => {
  r.innerHTML = r.innerHTML.replace('{lock}', I('lock','xs')).replace('{spark}', I('spark','xs')).replace(/\{x\}/g, I('x','xs'));
  const set = $('.bdx-rm', r), undo = $('.bdx-undo', r), live = $('.bdx-live', r);
  // the rows sit centred in their stage whatever the badges wrap to, and glide back to the middle as a remove or Undo
  // changes a row's height
  const box = $('.bdx', r); centre(c, box, () => box.offsetHeight);
  const label = b => $('.t', b).textContent;
  // each removable badge keeps its place in the row, so Undo puts it back where it was
  $$('.bdg', set).forEach((b, i) => { b.dataset.i = i; });
  // the badges taken off, the latest last
  const gone = [];
  // a remove button first sends a cancelable badge-remove event from its badge, so your code can keep it or act on it
  function remove(b){
    if (b.classList.contains('go')) return;
    if (!b.dispatchEvent(new CustomEvent('badge-remove', { bubbles:true, cancelable:true, detail:{ label:label(b) } }))) return;
    const had = b.contains(document.activeElement), all = $$('.bdg:not(.go)', set), i = all.indexOf(b), next = all[i + 1] || all[i - 1];
    gone.push(b); live.textContent = `Removed ${label(b)}`;
    // the focus goes on to the next badge's remove button, or to Undo once none is left
    const done = () => { b.remove(); b.classList.remove('go'); undo.hidden = false; if (had) ((next && next.isConnected && $('.bdg-x', next)) || undo).focus({ preventScroll:true }); };
    if (REDUCE) done(); else { b.classList.add('go'); c.later(220, done); }
  }
  function restore(){
    const b = gone.pop(); if (!b) return;
    const had = undo === document.activeElement, after = $$('.bdg', set).find(x => +x.dataset.i > +b.dataset.i);
    set.insertBefore(b, after || undo);
    b.classList.remove('pop-in'); void b.offsetWidth; b.classList.add('pop-in');
    undo.hidden = !gone.length; live.textContent = `${label(b)} is back`;
    if (had && undo.hidden) $('.bdg-x', b).focus({ preventScroll:true });
  }
  set.addEventListener('click', e => { const x = e.target.closest('.bdg-x'); if (x) remove(x.closest('.bdg')); });
  undo.onclick = restore;
  // the preview's story: take Staging off, then put it back with Undo
  c.story(async s => {
    await s.beat(4);
    const b = $$('.bdg', set).find(x => label(x) === 'Staging'); if (!b) return;
    await s.tap($('.bdg-x', b)); s.away(); await s.beat(4);
    await s.tap(undo); s.away(); await s.beat(4);
  });
});
06

Questions

What people ask about it.

How do I keep a badge when someone presses its ×?
In HTML, listen for badge-remove and call preventDefault(); the badge stays. In React, onRemove is yours: take the badge out of your list there, or don't.
Can I use my own colours?
Each tone sets two variables on the badge, --bdg-bg and --bdg-fg (--dm-bdg-bg and --dm-bdg-fg in React). Set them in your own class for a new colour, and check the text against its tint.
Why is the accent badge a tint and not a solid fill?
So it reads as a highlight, not a button. A solid accent fill is kept for the primary action.
Does the Context switcher use it?
It draws the same small badge, in the same tones, with its own copy of the styles, so each component installs on its own.
07

More in Forms and inputs

Buttons, fields, pickers and sign-in flows for any product.

ButtonsEvery button style, with loading, success and hold-to-confirm states.
TooltipQuiet labels on hover, with shortcuts and a richer preview card.
Search inputSearch with scoped filters, recent queries and an agent answer.
Country selectA searchable select with country codes, currency and keyboard support.
Multi-selectA combobox that filters as you type and keeps your picks as chips.
Social sign-inOne-tap providers that remember the last one you used.
Sign inEmail first, then the one-time code from the email, copied or typed in.
OTP fieldA one-time code, one box per digit, that fills from a paste.
All 11 in Forms and inputs →