fluent2-react-kit
uibadgesource: ok · hit
Badge
Fluent 2-styled badge with shadcn APIs. Six variants (default, secondary, destructive, outline, success, warning) as a compact pill, with icon support and asChild support.
npx shadcn add "https://fluent2-react-kit.graund.io/r/badge.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@radix-ui/react-slot
Registry dependencies
https://fluent2-react-kit.graund.io/r/utils.json
Files1 · source cached 10/8/2026, 5:58:00 PM
components/ui/badge.tsx
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
/**
* Badge — Fluent 2-styled, shadcn-API badge.
*
* Aesthetics follow Fluent 2 (a 4px-radius chip — Fluent's fixed `medium`
* corner token, non-scaling, NOT a full pill — compact 20px height, 10px/600
* caption-2 label with 4px horizontal padding, flat filled surfaces, brand/
* status color tokens); the prop surface (`variant`, `asChild`,
* `badgeVariants`) follows shadcn/ui.
*
* Geometry is spec-exact: `rounded-md` (`--radius-md` = 4px, the Fluent badge
* corner token used at every badge size), `px-1` (4px = `spacingHorizontalXS`),
* `text-[10px]/[14px]` (Fluent "Caption 2 Strong", == the `--text-caption2`
* token value — see the layout comment for why the arbitrary length is used
* over the `text-caption2` utility) + `font-semibold` (600), fixed `h-5`
* (20px Medium).
*
* Server-safe: no `"use client"`, no hooks/handlers — the React import is
* type-only, so the file can be dropped straight into an RSC tree.
*
* Hover/active state steps only engage when the badge is rendered as an
* interactive element via `asChild` (`[a&]:` targets the case where the
* rendered tag is an `<a>`); a plain status label stays static.
*/
const badgeVariants = cva(
[
// layout — Fluent 4px chip, compact 20px height, centered caption-2 label
"inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap",
// Fluent "Caption 2 Strong" = 10px/14px, weight 600. Expressed as the
// arbitrary `text-[10px]/[14px]` rather than the `text-caption2` token
// utility on purpose: every variant also sets a `text-*` color, and
// tailwind-merge (unaware of the custom `caption2` font-size) treats
// `text-caption2` as a text-color and drops it behind the variant color;
// the `--text-caption2` var also lives in `@theme inline`, which isn't
// emitted to :root, so a `text-(length:--text-caption2)` reference would be
// undefined at runtime. The arbitrary length is twMerge-safe and resolves
// with no runtime var. Values equal the `--text-caption2` token exactly.
"rounded-md border px-1 text-[10px]/[14px] font-semibold select-none",
// motion — token duration + Fluent easyEase curve; honors prefers-reduced-motion
// automatically (the duration tokens collapse to 0.01ms in tokens.css).
"outline-none transition-colors duration-fast ease-ease",
// focus — Fluent double-stroke approximation: 2px brand ring + 2px surface-colored gap.
// Only reachable when the badge is an interactive `asChild` element.
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
// invalid — shadcn aria-invalid treatment
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
// icon defaults
"[&>svg]:pointer-events-none [&>svg]:size-3",
],
{
variants: {
variant: {
// Fluent brand filled — flat, full brand state ramp on `asChild` links
// (spec §2.5): rest brand-80 → hover brand-70 → pressed brand-60 (light);
// rest brand-70 → hover brand-80 → pressed brand-60 (dark).
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-brand-70 [a&]:active:bg-brand-60 dark:[a&]:hover:bg-brand-80",
// Fluent standard/neutral — filled neutral surface + subtle border.
secondary:
"border-border bg-secondary text-secondary-foreground [a&]:hover:bg-accent [a&]:active:bg-input dark:[a&]:hover:bg-input",
// Fluent danger — red fill. No red ramp is exposed, so hover/pressed
// step via opacity; the focus ring switches to destructive.
destructive:
"border-transparent bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90 [a&]:active:bg-destructive/80 focus-visible:ring-destructive",
// Fluent outline — transparent + border, neutral hover.
outline:
"border-border bg-transparent text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground [a&]:active:bg-accent/80 dark:[a&]:hover:bg-input/50",
// Fluent success — green fill, AA-safe foreground from the status token pair.
success:
"border-transparent bg-success text-success-foreground [a&]:hover:bg-success/90 [a&]:active:bg-success/80",
// Fluent warning badge — bright orange chip (`#f7630c`) with DARK
// static text (`#242424`), the opposite foreground polarity from
// --warning's white text. Uses the dedicated `--warning-badge` pair
// (~4.98:1 AA), matching Figma's WarningBackground3 + NeutralForeground1
// Static, so Alert's darkOrange --warning stays put (single-source
// decoupling; see tokens.css + tokens-research.md §12.9).
warning:
"border-transparent bg-warning-badge text-warning-badge-foreground [a&]:hover:bg-warning-badge/90 [a&]:active:bg-warning-badge/80",
},
},
defaultVariants: {
variant: "default",
},
}
);
function Badge({
className,
variant = "default",
asChild = false,
...props
}: ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant, className }))}
{...props}
/>
);
}
export { Badge, badgeVariants };