# Badge

> A short static label that classifies an item, such as Beta or a category name. Six tones, soft, solid and outline fills, two fixed heights, an optional icon or dot; one line, truncated when too long.

- ID: `cmp_badge_01`
- Slug: `badge-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-10-01T08:13:00Z
- Updated: 2026-10-01
- Available versions: `1.0.0`
- Kind: control
- Primary category: `typography`
- Detail page: https://pagesugar.com/components/badge-01?variant=blue
- Preview: https://pagesugar.com/preview/badge-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `neutral` | Neutral | yes | `sha256-b2a4f6e88f9592daf3fde04cfd15acc01a18028a09bb9e436fa607a87ae58c6b` |
| `violet` | Violet accent | no | `sha256-6aa8e16f272a684e0c6dd813af49589cf5d3c5b1b3c064d36a2026f020db4836` |
| `blue` | Blue accent | no | `sha256-36da6ac16a8b43f8ab1017b0715c8fbd4c317fe8cfaf4e5339f54e9eadb33340` |

## Runtime and compatibility

- Runtime: svelte
- Svelte: 5
- SvelteKit required: no (portable Svelte component)
- Tailwind CSS: 4
- SSR: supported
- Requires client-side JavaScript: no
- Integration level: presentational
- Appearance modes: light
- Suggested directory: `src/lib/components/badge-01`

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Put the label in children and pick a tone, variant and size; add an icon snippet or dot for a leading mark. It renders one span of static text. It is not a button or link, has no remove action, does not count or update live, and does not map data states to tones (use a status badge or tag chip for those).

Required props: `children`

```svelte
<script lang="ts">
	import Badge from '$lib/components/badge-01/Badge.svelte';
</script>

<h3>
	Guest access <Badge class="ms-2">Beta</Badge>
</h3>

<Badge tone="success" dot>Shipped</Badge>
<Badge tone="accent" variant="solid" size="sm">New</Badge>
<Badge tone="warning" variant="outline">
	{#snippet icon()}
		<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="5.5" /><path d="M8 5v3l2 1.25" /></svg>
	{/snippet}
	Due Friday
</Badge>
```

Limitations:

- The label never wraps. Past the width of its container it truncates with an ellipsis, so keep labels to one or two words; a 40-character label in a narrow card shows only its start.
- Colour is a hint, not the message: the tone does not add any text, so the label itself has to say what it means.
- The accent tone's soft and outline fills use the accent as text colour. A light accent (yellow, a pastel) will fail contrast there; use the solid variant with a dark --badge-on-accent instead.
- Light mode only. The tints are mixed with transparency, so they work on white, zinc-50 and zinc-100 pages; on a dark page set --badge-ink, --badge-muted and each tone to lighter colours.
- The badge sits on the text baseline. Next to a large display heading you may want vertical-align: middle or a small raise through the class prop.

## Usage guide

### Badge

A badge is a label, not a control. Use it to say what kind of thing an item is ("Beta",
"Business plan", "Vegan") or to flag one notable fact about it. It never changes on its own and
never does anything when clicked.

#### Picking a tone

Most badges should be **neutral**. Colour earns its place when the label maps to a meaning a
reader already knows: success for done or safe, warning for "look before you go on", danger for
blocked or destructive, info for neutral system facts. **Accent** is for the one label you want
to pull the eye, such as "New" on a navigation item. A panel where every row carries a coloured
badge has no exceptions left to point at.

#### Picking a variant

- **Soft** (default): a tint with a hairline ring. Quiet enough to sit beside body text.
- **Solid**: the strongest. One per view, at most.
- **Outline**: a ring only, for secondary facts beside a soft badge, such as a plan name.

#### In a heading or a row

```svelte
<h3 class="text-base font-semibold">
	Guest access <Badge class="ms-2">Beta</Badge>
</h3>
```

The badge is `inline-flex` and sits on the text baseline. Keep a space or margin between it and
the text. Inside a flex row, put it after the label and let the row's `items-center` align it.

#### Data-driven badges

If the label comes from a status field (`draft`, `in_review`, `merged`) and the tone should follow
it, map the field to a label and tone in your own code, or use a status badge component that does
the mapping for you. This component deliberately takes neither a status key nor a count.

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `tone` | 'neutral' \| 'accent' \| 'info' \| 'success' \| 'warning' \| 'danger' | no | `'neutral'` | Colour role. Neutral suits most labels; accent follows --badge-accent. The text, not the colour, carries the meaning. |
| `variant` | 'soft' \| 'solid' \| 'outline' | no | `'soft'` | Fill style: a tint with a hairline ring, a solid fill, or a ring alone. All three keep the same box size. |
| `size` | 'sm' \| 'md' | no | `'md'` | sm is 20 px tall with 12 px text, for tables and dense rows; md is 24 px with 13 px text. |
| `dot` | `boolean` | no | `false` | Show a small dot in the tone's colour before the label. Ignored when an icon is given. |
| `icon` | `Snippet` | no |  | Leading decorative icon, sized to 12 or 14 px and coloured by the tone. Hidden from assistive technology. |
| `class` | `string` | no |  | Extra classes on the badge, for margins or a tighter max-width. |
| `children` | `Snippet` | yes |  | The label: one or two words. |

## Customization

Choose tone, variant and size through props. Retone with --badge-\* variables: one per tone, and the tints and rings follow. Edit the TONES, VARIANTS and SIZES maps in the source to change the recipe.

- Brand colour: set --badge-accent (and --badge-on-accent for the solid fill's text) on the page or any ancestor. The accent tone's tint and ring are mixed from it, so all three variants follow.
- Semantic colours: --badge-info, --badge-success, --badge-warning and --badge-danger each set one tone. The label must clear 4.5:1 against each variant's own background: the page for outline, the 8% tint of the colour for soft (a colour that only just passes on white fails there), and the colour itself against white text for solid.
- Neutral tone: --badge-ink is the label colour, --badge-muted the colour of its dot or icon, and --badge-hairline its soft ring.
- Dark or tinted page: set --badge-ink to zinc-100, --badge-muted to zinc-400, --badge-hairline to rgb(255 255 255 / 0.12), and each tone to its 300 or 400 step. Solid fills then want a dark label: change the #ffffff values in TONES to zinc-950.
- Shape: the badge is a 4 px (sm) or 6 px (md) rectangle. For a pill, change rounded-sm and rounded-md to rounded-full in SIZES.
- In a heading: put the badge after the text with a margin (class="ms-2"). It sits on the baseline; add align-middle through class if you prefer it centred on a large title.
- Width: pass class="max-w-40" to truncate earlier in a dense table.

| Token | Public CSS variable |
| --- | --- |
| `accent` | `--badge-accent` |
| `onAccent` | `--badge-on-accent` |
| `ink` | `--badge-ink` |
| `muted` | `--badge-muted` |
| `hairline` | `--badge-hairline` |
| `info` | `--badge-info` |
| `success` | `--badge-success` |
| `warning` | `--badge-warning` |
| `danger` | `--badge-danger` |

## Accessibility

- Renders a plain span with no role and no live region: a badge is static text read in place, so it is not announced on its own.
- The label is the meaning. Tone colour only reinforces it, so never rely on a colour difference (a green Shipped and a red Shipped) to say something the text does not.
- Icons and dots are aria-hidden. If an icon carries meaning the label does not, put that meaning in the label.
- A truncated label is cut visually only; the whole text stays in the DOM and is read in full. Sighted users see only the start, so keep labels short or give the full text elsewhere.
- Every tone and variant clears 4.5:1 for its label in the default palette on white, zinc-50 and zinc-100 pages. Re-check if you set a tone variable.
- Not focusable and not interactive. For a removable or clickable token use a tag chip, which brings its own button semantics.

Known limitations:

- Contrast is checked for the default colours on light pages only; retoned or dark pages are the consumer's to check.

## License

- Declared source: MIT
- Default license approval is pending. See https://pagesugar.com/docs/license.

## Source

- Palette: Blue accent (`blue`)
- Entry: `Badge.svelte`
- Suggested directory: `src/lib/components/badge-01`
- Files: 1
- Artifact digest: `sha256-36da6ac16a8b43f8ab1017b0715c8fbd4c317fe8cfaf4e5339f54e9eadb33340`

Paths below are relative to the suggested directory. Copy the files as they are;
they import nothing from this site.

#### `Badge.svelte`

Role: entry · 5214 bytes · SHA-256 `3b2912d67d7c40557f398164fddbb25c3592c727e7fb0d2d6e9a2c00cdc917a9`

```svelte
<script lang="ts" module>
	export type BadgeTone = 'neutral' | 'accent' | 'info' | 'success' | 'warning' | 'danger';
	export type BadgeVariant = 'soft' | 'solid' | 'outline';
	export type BadgeSize = 'sm' | 'md';
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';

	interface Props {
		/** Colour role. Neutral by default; the text, not the colour, carries the meaning. */
		tone?: BadgeTone;
		/** Fill style: a tint, a solid fill, or a hairline outline. */
		variant?: BadgeVariant;
		/** Height and text size: sm is 20 px for tables and dense rows, md is 24 px. */
		size?: BadgeSize;
		/** Show a small dot before the label, in the tone's colour. Ignored when icon is set. */
		dot?: boolean;
		/** Leading decorative icon, sized by the badge; hidden from assistive technology. */
		icon?: Snippet;
		/** Extra classes on the badge, for margins or a tighter max-width. */
		class?: string;
		/** The label: one or two words. Long labels truncate rather than wrap. */
		children: Snippet;
	}

	let {
		tone = 'neutral',
		variant = 'soft',
		size = 'md',
		dot = false,
		icon,
		class: className,
		children
	}: Props = $props();

	/*
	 * Each tone is one ink. The tint, the hairline ring and the solid fill are all mixed from it
	 * in the style block, so soft, solid and outline agree on the colour and never change the
	 * box. Complete class names, never interpolated.
	 */
	const TONES: Record<BadgeTone, string> = {
		neutral: 'badge--neutral [--_tone:var(--_ink)] [--_mark:var(--_muted)] [--_on-tone:#ffffff]',
		accent: '[--_tone:var(--_accent)] [--_mark:var(--_accent)] [--_on-tone:var(--_on-accent)]',
		info: '[--_tone:var(--_info)] [--_mark:var(--_info)] [--_on-tone:#ffffff]',
		success: '[--_tone:var(--_success)] [--_mark:var(--_success)] [--_on-tone:#ffffff]',
		warning: '[--_tone:var(--_warning)] [--_mark:var(--_warning)] [--_on-tone:#ffffff]',
		danger: '[--_tone:var(--_danger)] [--_mark:var(--_danger)] [--_on-tone:#ffffff]'
	};

	const VARIANTS: Record<BadgeVariant, { root: string; mark: string }> = {
		soft: {
			root: 'bg-(--_tint) text-(--_tone) inset-ring inset-ring-(--_ring)',
			mark: 'text-(--_mark)'
		},
		solid: { root: 'bg-(--_tone) text-(--_on-tone)', mark: 'text-(--_on-tone)' },
		outline: { root: 'text-(--_tone) inset-ring inset-ring-(--_outline)', mark: 'text-(--_mark)' }
	};

	/*
	 * The label's line box is the badge's full height, so it sits centred and is the one item
	 * aligned by baseline: the badge then shares the label's baseline with the text around it,
	 * whether or not an icon or dot leads. The side with an icon or dot sits closer to the
	 * edge, so the mark does not look indented.
	 */
	const SIZES: Record<BadgeSize, { root: string; bare: string; marked: string; mark: string }> = {
		sm: {
			root: 'h-5 gap-1 rounded-sm text-xs leading-5',
			bare: 'px-1.5',
			marked: 'ps-1 pe-1.5',
			mark: 'size-3'
		},
		md: {
			root: 'h-6 gap-1.5 rounded-md text-[0.8125rem] leading-6',
			bare: 'px-2',
			marked: 'ps-1.5 pe-2',
			mark: 'size-3.5'
		}
	};

	const step = $derived(SIZES[size] ?? SIZES.md);
	const look = $derived(VARIANTS[variant] ?? VARIANTS.soft);
	const marked = $derived(Boolean(icon) || dot);
</script>

<span
	class={[
		'badge inline-flex max-w-full min-w-0 items-center font-medium tracking-normal whitespace-nowrap tabular-nums',
		TONES[tone] ?? TONES.neutral,
		look.root,
		step.root,
		marked ? step.marked : step.bare,
		className
	]}
>
	{#if icon}
		<span
			class={['badge__icon inline-flex shrink-0 items-center justify-center', look.mark, step.mark]}
			aria-hidden="true"
		>
			{@render icon()}
		</span>
	{:else if dot}
		<span
			class={['inline-flex shrink-0 items-center justify-center', look.mark, step.mark]}
			aria-hidden="true"
		>
			<span class="size-1.5 rounded-full bg-current"></span>
		</span>
	{/if}
	<span class="badge__label min-w-0 self-baseline truncate">{@render children()}</span>
</span>

<style>
	/*
	 * Public tokens: set --badge-* on the badge or any ancestor to retone it. Each tone is one
	 * colour; its tint and rings are mixed from it below, so one variable retones all three
	 * variants. The defaults clear 4.5:1 for the label on white, zinc-50 and zinc-100 pages.
	 */
	.badge {
		--_accent: var(--badge-accent, #1d4ed8);
		--_on-accent: var(--badge-on-accent, #ffffff);
		--_ink: var(--badge-ink, #18181b);
		--_muted: var(--badge-muted, #52525b);
		--_hairline: var(--badge-hairline, rgb(0 0 0 / 0.1));
		--_info: var(--badge-info, #0369a1);
		--_success: var(--badge-success, #046c4e);
		--_warning: var(--badge-warning, #9a4a00);
		--_danger: var(--badge-danger, #b91c1c);

		/* Formulas: the soft tint, its ring, and the outline's stronger ring, from one ink. */
		--_tint: color-mix(in srgb, var(--_tone) 8%, transparent);
		--_ring: color-mix(in srgb, var(--_tone) 14%, transparent);
		--_outline: color-mix(in srgb, var(--_tone) 24%, transparent);
	}

	/* The neutral tint is already close to black at 8%, so its ring is the hairline token. */
	.badge--neutral {
		--_ring: var(--_hairline);
	}

	/* Icons passed in as a snippet fill the mark box and follow its colour. */
	.badge__icon :global(:where(svg)) {
		width: 100%;
		height: 100%;
	}
</style>
```

## Artifacts

### Blue accent (`blue`)

- Artifact digest: `sha256-36da6ac16a8b43f8ab1017b0715c8fbd4c317fe8cfaf4e5339f54e9eadb33340`
- Entry: `Badge.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_badge_01/1.0.0/blue/sha256-36da6ac16a8b43f8ab1017b0715c8fbd4c317fe8cfaf4e5339f54e9eadb33340/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_badge_01/1.0.0/blue/sha256-36da6ac16a8b43f8ab1017b0715c8fbd4c317fe8cfaf4e5339f54e9eadb33340/bundle.zip (4884 bytes, sha256 `2788ed1ca1c78bde924e78865dc64e9321f59b7c9adb904f297cf83d4697ee93`)

Files:

- `Badge.svelte` (entry, 5214 bytes): https://pagesugar.com/artifacts/cmp_badge_01/1.0.0/blue/sha256-36da6ac16a8b43f8ab1017b0715c8fbd4c317fe8cfaf4e5339f54e9eadb33340/source/Badge.svelte
