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.
cmp_badge_01 Preview
Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.
Give this component to your coding agent Copy a prompt that fetches this exact version and palette through the PageSugar MCP server.
Using the PageSugar MCP server, fetch component cmp_badge_01 version 1.0.0 with variant "violet", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.Not connected yet? Set up the MCP server
Code
- Palette
- Violet accent
- Version
- 1.0.0
- Digest
Full digest
sha256-6aa8e16f272a684e0c6dd813af49589cf5d3c5b1b3c064d36a2026f020db4836
<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, #6d28d9);
--_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>
Usage#
On this pagePut 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).
- Suggested location
src/lib/components/badge-01- Required props
children
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-accentinstead. - Light mode only. The tints are mixed with transparency, so they work on white,
zinc-50andzinc-100pages; on a dark page set--badge-ink,--badge-mutedand 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.
Example
<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>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#
<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 and content inputs#
On this page| 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 | None | Leading decorative icon, sized to 12 or 14 px and coloured by the tone. Hidden from assistive technology. |
class | string | No | None | Extra classes on the badge, for margins or a tighter max-width. |
children | Snippet | Yes | None | The label: one or two words. |
Customization#
On this pageChoose 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-accentfor 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-warningand--badge-dangereach 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-inkis the label colour,--badge-mutedthe colour of its dot or icon, and--badge-hairlineits soft ring. - Dark or tinted page: set
--badge-inktozinc-100,--badge-mutedtozinc-400,--badge-hairlineto rgb(255 255 255 / 0.12), and each tone to its 300 or 400 step. Solid fills then want a dark label: change the#ffffffvalues in TONES tozinc-950. - Shape: the badge is a 4 px (sm) or 6 px (md) rectangle. For a pill, change
rounded-smandrounded-mdtorounded-fullin 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.
Public CSS variables
| Variable | Token |
|---|---|
--badge-accent | accent |
--badge-on-accent | onAccent |
--badge-ink | ink |
--badge-muted | muted |
--badge-hairline | hairline |
--badge-info | info |
--badge-success | success |
--badge-warning | warning |
--badge-danger | danger |
Accessibility#
On this page- 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-50andzinc-100pages. 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.
Release details#
On this page- Integration
- Presentational
- Works without client-side JavaScript
- Server-side rendering supported
- Dependencies
- No additional runtime packages beyond Svelte and Tailwind CSS
- License
MIT. Default license approval is pending; see the license status before adopting the source.
- Version history
- 1.0.0 (Published) Current release · 1 October 2026
Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.