Badge
SourceA small label for status, categorization, or metadata — a pill chip that sits inline with text or in a row of its own. Its look is two orthogonal axes: variant sets the emphasis and tone sets the colour, so a quiet metadata chip and a loud status share one component.
Basic
A bare Badge.Root is a quiet soft neutral chip — the default metadata label. Pass children as the text.
"use client";
import { Badge } from "@stridge/noctis/badge";
export default function BadgeBasic() {
return <Badge.Root>New</Badge.Root>;
}
Variants
variant is the emphasis axis: a filled solid chip, a quiet soft tint (the default), or a transparent outline with a coloured edge. It is orthogonal to tone — every variant works with every tone.
"use client";
import { Badge } from "@stridge/noctis/badge";
const VARIANTS: Badge.Variant[] = ["solid", "soft", "outline"];
export default function BadgeVariants() {
return (
<div className="flex flex-wrap items-center gap-2">
{VARIANTS.map((variant) => (
<Badge.Root key={variant} variant={variant} tone="accent">
{variant}
</Badge.Root>
))}
</div>
);
}
Semantic tones
tone sets the colour identity. The six semantic tones carry meaning — neutral (the default), accent, and the four statuses success, warning, danger, and info — and paint from their role families, so a retheme propagates. Reach for these when the colour signals state.
"use client";
import { Badge } from "@stridge/noctis/badge";
const SEMANTIC: Badge.Tone[] = ["neutral", "accent", "success", "warning", "danger", "info"];
export default function BadgeTones() {
return (
<div className="flex flex-wrap items-center gap-2">
{SEMANTIC.map((tone) => (
<Badge.Root key={tone} tone={tone} variant="soft">
{tone}
</Badge.Root>
))}
</div>
);
}
Decorative colours
The eight decorative tones — red, orange, amber, green, teal, blue, purple, pink — are accent-independent category colours drawn from the shared categorical palette. Use them to distinguish categories (a product, a label, a team), not to signal status — they carry no semantic meaning and never re-derive off the accent. They read best as solid.
"use client";
import { Badge } from "@stridge/noctis/badge";
const DECORATIVE: Badge.Tone[] = ["red", "orange", "amber", "green", "teal", "blue", "purple", "pink"];
export default function BadgeDecorative() {
return (
<div className="flex flex-wrap items-center gap-2">
{DECORATIVE.map((tone) => (
<Badge.Root key={tone} tone={tone} variant="solid">
{tone}
</Badge.Root>
))}
</div>
);
}
Status dot
Compose a leading Badge.Dot for an at-a-glance status dot. It is decorative (aria-hidden) — pair it with a label like "Healthy" so the meaning is never colour-only — and fills with the chip's text colour, so it reads as a knockout on a solid chip and as the saturated tone on a soft/outline one.
"use client";
import { Badge } from "@stridge/noctis/badge";
export default function BadgeDot() {
return (
<div className="flex flex-wrap items-center gap-2">
<Badge.Root variant="soft" tone="success">
<Badge.Dot />
Healthy
</Badge.Root>
<Badge.Root variant="soft" tone="warning">
<Badge.Dot />
Degraded
</Badge.Root>
<Badge.Root variant="outline" tone="danger">
<Badge.Dot />
Offline
</Badge.Root>
</div>
);
}
Dot badges
The dot variant is a quieter take: the chip stays neutral — a transparent fill, a hairline border, neutral text for every tone — and the leading Badge.Dot alone carries the status colour. Reach for it when a row of statuses shouldn't each shout a full-colour chip; the dot still reads at a glance while the labels stay calm.
"use client";
import { Badge } from "@stridge/noctis/badge";
export default function BadgeDotBadges() {
return (
<div className="flex flex-wrap items-center gap-2">
<Badge.Root variant="dot" tone="success">
<Badge.Dot />
Healthy
</Badge.Root>
<Badge.Root variant="dot" tone="warning">
<Badge.Dot />
Warning
</Badge.Root>
<Badge.Root variant="dot" tone="danger">
<Badge.Dot />
Error
</Badge.Root>
<Badge.Root variant="dot" tone="neutral">
<Badge.Dot />
Neutral
</Badge.Root>
</div>
);
}
Icon
Compose a leading Badge.Icon for a glyph. It clamps the icon to the chip's size and inherits its colour. Wrap an <Icon> (or any inline svg) inside it — don't put data-slot on the icon itself, which would drop its own token sizing.
"use client";
import { Icon } from "@stridge/noctis";
import { Badge } from "@stridge/noctis/badge";
import { Check } from "lucide-react";
export default function BadgeIcon() {
return (
<Badge.Root variant="soft" tone="success">
<Badge.Icon>
<Icon icon={Check} />
</Badge.Icon>
Verified
</Badge.Root>
);
}
Sizes
size is md (the default) or sm — the smaller chip for dense rows or inline-with-text use. The label type and the dot/icon scale with it.
"use client";
import { Badge } from "@stridge/noctis/badge";
const SIZES: Badge.Size[] = ["sm", "md"];
export default function BadgeSizes() {
return (
<div className="flex items-center gap-2">
{SIZES.map((size) => (
<Badge.Root key={size} size={size} tone="accent">
{size}
</Badge.Root>
))}
</div>
);
}
In a sentence
A sm badge sits proportionally beside running text — a "New" or "Beta" marker after a feature name.
WorkersNew
"use client";
import { Badge } from "@stridge/noctis/badge";
export default function BadgeInSentence() {
return (
<p className="inline-flex items-center gap-2 text-sm">
Workers
<Badge.Root size="sm">New</Badge.Root>
</p>
);
}
Accessibility
A badge is a label, not a control — its text content is its accessible name, so most badges need nothing extra. The Badge.Dot and Badge.Icon are decorative (aria-hidden), so a status that's signalled by colour or a dot must also be in the label text ("Online", not a bare green dot). For a standalone icon-only chip, give Badge.Root an aria-label. Rendering a badge as an <a> (via Badge.Root.props(...)) keeps it keyboard-reachable as a link.
Anatomy
Compose the chip from its parts. Badge.Root renders a <span> by default; spread Badge.Root.props(...) onto an <a> (or any element) to style it as a chip.
Badge.Root— the pill; ownsvariant(emphasis),tone(colour), andsize.Badge.Dot— an optional leading status dot. Decorative; fills with the chip'scurrentColor.Badge.Icon— an optional leading glyph. Decorative; clamps its icon to the chip's icon size and inherits its colour.
Every rendered part carries a data-slot (noctis-badge on the chip, noctis-badge-dot on the dot, noctis-badge-icon on the icon wrapper) for host-side styling — pair it with the data-variant/data-tone/data-size axes the root stamps, off which the colour grid and per-size metrics are keyed.
Theme scope
ThemeScope re-tunes a whole region's shape — corner radius, density, and type scale — through the cascade, with no per-component prop and without touching the app-wide theme. Tweak the knobs below and watch every Badge inside the scope retune together. See Customization for nesting, reset, and the portal caveat.
"use client";
import { DENSITY_PRESETS, FONT_SCALE_PRESETS, RADIUS_PRESETS, ThemeScope } from "@stridge/noctis";
import { Badge } from "@stridge/noctis/badge";
import { Select } from "@stridge/noctis/select";
import { useState } from "react";
/** Title-case a preset key for its select label. */
const label = (key: string) => key.charAt(0).toUpperCase() + key.slice(1);
/** One labelled preset picker — its options are a shipped `ThemeScope` preset map, so the playground's
* vocab is exactly what the primitive accepts. */
function Knob({
name,
presets,
value,
onChange,
}: {
name: string;
presets: Record<string, number>;
value: string;
onChange: (value: string) => void;
}) {
const keys = Object.keys(presets);
const items = Object.fromEntries(keys.map((key) => [key, label(key)]));
return (
<label className="flex flex-col gap-1.5">
<span className="text-mini font-medium text-subtle">{name}</span>
<Select.Root items={items} value={value} onValueChange={(next) => onChange(String(next))}>
<Select.Trigger aria-label={name} className="w-40">
<Select.Value />
<Select.Icon />
</Select.Trigger>
<Select.Popup>
{keys.map((key) => (
<Select.Item key={key} value={key}>
{label(key)}
</Select.Item>
))}
</Select.Popup>
</Select.Root>
</label>
);
}
/**
* A live `ThemeScope` playground: the three seed knobs (radius, density, type scale) drive a scope
* wrapping a badge cluster, so every control inside re-tunes together — corners, spacing, and text
* size — with no per-badge prop and without touching the app-wide theme.
*/
export default function BadgeThemeScope() {
const [radius, setRadius] = useState<keyof typeof RADIUS_PRESETS>("pill");
const [density, setDensity] = useState<keyof typeof DENSITY_PRESETS>("default");
const [fontScale, setFontScale] = useState<keyof typeof FONT_SCALE_PRESETS>("default");
return (
<div className="flex flex-col gap-5">
<div className="flex flex-wrap gap-3">
<Knob name="Radius" presets={RADIUS_PRESETS} value={radius} onChange={(v) => setRadius(v as typeof radius)} />
<Knob
name="Density"
presets={DENSITY_PRESETS}
value={density}
onChange={(v) => setDensity(v as typeof density)}
/>
<Knob
name="Font scale"
presets={FONT_SCALE_PRESETS}
value={fontScale}
onChange={(v) => setFontScale(v as typeof fontScale)}
/>
</div>
<div className="rounded-md border border-dashed border-border p-4">
<ThemeScope radius={radius} density={density} fontScale={fontScale}>
<div className="flex flex-wrap items-center gap-2">
<Badge.Root tone="success" variant="soft">
Success
</Badge.Root>
<Badge.Root tone="warning" variant="soft">
Warning
</Badge.Root>
<Badge.Root tone="danger" variant="solid">
Danger
</Badge.Root>
<Badge.Root tone="accent" variant="outline">
Accent
</Badge.Root>
</div>
</ThemeScope>
</div>
</div>
);
}
On surfaces
The same chip re-tuned across the elevation scopes — the root canvas, an elevated panel, a menu, and a sunken well. It stays legible on every layer.
Design tokens
Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every badge in that region retunes — e.g. .tags { --noctis-badge-border-radius: var(--noctis-radius-sm); } squares the chips beneath it. Colours aren't minted — the grid reads the semantic roles and the shared categorical palette directly, so a retheme propagates for free. See Customization for the full override ladder and Tokens for the whole graph.
API reference
Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table. Expand a row for the full type and description.