Siteplane UI

Badge

Label a status, category, or count.

Markdown
Draft
"use client";

import { Badge } from "@/siteplane/components/badge";

export default function Example() {
  return <Badge>Draft</Badge>;
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/badge

Complete the one-time setup first. Registry JSON lists source files and dependencies.

Usage

import { Badge } from "@/siteplane/components/badge";

<Badge>Draft</Badge>;

Variants

default is a white surface with a hairline contour. outline keeps only the contour, so whatever the badge sits on shows through: it matches default on white and suits chips inside fields and on tinted surfaces. secondary is a light, translucent neutral fill. accent (the brand color), inverse (black in Light) and destructive are solid fills for emphasis. info, success, warning and error tint the badge with a status color; info is the tint of the accent. Use a visible label as well as color to communicate status.

DefaultSecondaryOutlineAccentInverseDestructiveInfoSuccessWarningError
"use client";

import { Badge } from "@/siteplane/components/badge";

export default function Example() {
  return (
    <div className="flex flex-wrap gap-2">
      <Badge>Default</Badge>
      <Badge variant="secondary">Secondary</Badge>
      <Badge variant="outline">Outline</Badge>
      <Badge variant="accent">Accent</Badge>
      <Badge variant="inverse">Inverse</Badge>
      <Badge variant="destructive">Destructive</Badge>
      <Badge variant="info">Info</Badge>
      <Badge variant="success">Success</Badge>
      <Badge variant="warning">Warning</Badge>
      <Badge variant="error">Error</Badge>
    </div>
  );
}

Secondary

secondary is the quiet neutral fill, mixed from the kit's tokens: the muted grey at 60 % in Light, and 6 % of the text color in Dark, which lightens the surface it sits on. It stays translucent, so it reads on a white card, on a tinted row and on a dark card, and suits labels that sit beside other content without competing with it, such as the sections of a site in a list.

  • northlight.studioPagesBlogShop
  • copperleaf.spaPagesBookings
"use client";

import { Badge } from "@/siteplane/components/badge";

const sites = [
  {
    current: true,
    name: "northlight.studio",
    sections: ["Pages", "Blog", "Shop"],
  },
  {
    current: false,
    name: "copperleaf.spa",
    sections: ["Pages", "Bookings"],
  },
] as const;

export default function Example() {
  return (
    <ul aria-label="Sites" className="grid w-full max-w-md gap-1 text-sm">
      {sites.map((site) => (
        <li
          aria-current={site.current ? "true" : undefined}
          className={`flex min-w-0 items-center justify-between gap-4 rounded-md px-3 py-2 ${
            site.current ? "bg-[var(--sp-rw-bg-accent-translucent)]" : ""
          }`}
          key={site.name}
        >
          <span className="min-w-0 truncate" title={site.name}>
            {site.name}
          </span>
          <span className="flex shrink-0 gap-1">
            {site.sections.map((section) => (
              <Badge key={section} variant="secondary">
                {section}
              </Badge>
            ))}
          </span>
        </li>
      ))}
    </ul>
  );
}

Emphasis

accent and inverse are solid, so keep them small and few. Use accent to flag something new and inverse for a count. A count needs a contextual accessible name, here as hidden text.

Release notesNewInbox12 unread messages
"use client";

import { Badge } from "@/siteplane/components/badge";

export default function Example() {
  return (
    <div className="flex flex-wrap items-center gap-x-8 gap-y-3 text-sm">
      <span className="inline-flex items-center gap-2">
        Release notes
        <Badge size="sm" variant="accent">
          New
        </Badge>
      </span>
      <span className="inline-flex items-center gap-2">
        Inbox
        <Badge size="sm" variant="inverse">
          12
          <span className="sr-only"> unread messages</span>
        </Badge>
      </span>
    </div>
  );
}

Sizes

sm, md, lg and xl badges are 16, 20, 24 and 28 px high, and their type and icons grow with them. The omitted size inherits the provider control size. Without an override it is lg.

SmallMediumLargeExtra large
"use client";

import { Badge } from "@/siteplane/components/badge";

export default function Example() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Badge size="sm">Small</Badge>
      <Badge size="md">Medium</Badge>
      <Badge size="lg">Large</Badge>
      <Badge size="xl">Extra large</Badge>
    </div>
  );
}

Badge text takes the weight --sp-rw-badge-font-weight, which is unset and falls back to 450. Set it after the base import to give every badge another weight, for example to match another typeface.

Long labels

A badge is never wider than the space it sits in. Its text stays on one line and ends with an ellipsis where the badge meets the edge of that space, and the full text is the label's title, so a long value never pushes a card, a list row or a group of tags apart. In a table cell, a tab or a button, where text stays on one line, the badge keeps its full width like the text beside it. A count never truncates: the count profile grows as a pill, at least as wide as it is high.

Shape

shape="pill" rounds a badge fully at every size, like the pill Button. Without it, a badge takes the theme's badge radius, --sp-rw-radius-badge, which is the small --sp-rw-radius-xs by default. To change every badge at once, set the token in your theme.

DefaultPill
"use client";

import { Badge } from "@/siteplane/components/badge";

export default function Example() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Badge>Default</Badge>
      <Badge shape="pill">Pill</Badge>
    </div>
  );
}

With icon

Place a decorative icon before the label; the badge sizes it automatically.

VerifiedScheduled
"use client";

import { CheckIcon, ClockIcon } from "lucide-react";
import { Badge } from "@/siteplane/components/badge";

export default function Example() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Badge variant="success">
        <CheckIcon aria-hidden="true" />
        Verified
      </Badge>
      <Badge variant="outline">
        <ClockIcon aria-hidden="true" />
        Scheduled
      </Badge>
    </div>
  );
}

With status dot

Add a small dot before the label to make statuses easy to scan in lists. The label carries the meaning, so hide the dot from assistive technology.

PaidPendingFailed
"use client";

import { Badge } from "@/siteplane/components/badge";

export default function Example() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Badge variant="outline">
        <span aria-hidden="true" className="size-1.5 rounded-full bg-success" />
        Paid
      </Badge>
      <Badge variant="outline">
        <span aria-hidden="true" className="size-1.5 rounded-full bg-warning" />
        Pending
      </Badge>
      <Badge variant="outline">
        <span
          aria-hidden="true"
          className="size-1.5 rounded-full bg-destructive"
        />
        Failed
      </Badge>
    </div>
  );
}

Status

profile="status" writes a state in words after a colored dot, without fill or contour, so it reads well in a list row or in running text. The variant gives the dot its tone: success, warning, error or destructive, and info or accent; every other variant shows the gray of an offline Avatar. The words carry the state in the secondary text color, at the badge's size and weight, and the dot repeats it, hidden from assistive technology. The dot grows with the size, 6 px at sm and md and 8 px at lg and xl, with as much space before the words.

  • northlight.studioLive
  • shop.northlight.studioBuilding
  • docs.northlight.studioNeeds attention
  • staging.northlight.studioFailed
  • archive.northlight.studioPaused

The checkout is Live again since the last deploy.

"use client";

import { Badge, type BadgeVariant } from "@/siteplane/components/badge";

const sites: { name: string; status: string; tone: BadgeVariant }[] = [
  { name: "northlight.studio", status: "Live", tone: "success" },
  { name: "shop.northlight.studio", status: "Building", tone: "info" },
  {
    name: "docs.northlight.studio",
    status: "Needs attention",
    tone: "warning",
  },
  { name: "staging.northlight.studio", status: "Failed", tone: "error" },
  { name: "archive.northlight.studio", status: "Paused", tone: "default" },
];

export default function Example() {
  return (
    <div className="grid w-full max-w-md gap-4 text-sm">
      <ul aria-label="Sites" className="grid gap-3">
        {sites.map((site) => (
          <li
            className="flex min-w-0 items-center justify-between gap-4"
            key={site.name}
          >
            <span className="min-w-0 truncate" title={site.name}>
              {site.name}
            </span>
            <Badge profile="status" variant={site.tone}>
              {site.status}
            </Badge>
          </li>
        ))}
      </ul>
      <p className="text-muted-foreground">
        The checkout is{" "}
        <Badge profile="status" variant="success">
          Live
        </Badge>{" "}
        again since the last deploy.
      </p>
    </div>
  );
}

Use render to style a link as a badge. It keeps link semantics and gains hover and focus styles; otherwise a badge is a non-interactive span.

"use client";

import { Badge } from "@/siteplane/components/badge";

export default function Example() {
  return <Badge render={<a href="#changelog" />}>Release notes</Badge>;
}

Light

Give the one announcement on a page a light: two soft lights, the brand accent and an amber highlight, run around the badge's edge. Use it on an opaque badge such as the default one, at most once per page. Reduced motion keeps the lights still, and hover makes the glow a little stronger instead of tinting the badge.

"use client";

import { ArrowRightIcon } from "lucide-react";

import { Badge } from "@/siteplane/components/badge";

export default function Example() {
  return (
    <Badge light render={<a href="/docs/changelog/" />} shape="pill" size="xl">
      New: Charts
      <ArrowRightIcon aria-hidden="true" />
    </Badge>
  );
}

API reference

Component properties
PropTypeDefaultDescription
variantdefault | secondary | outline | accent | inverse | destructive | info | success | warning | errordefaultWhite with a hairline (default), neutral (the light, translucent secondary and outline), solid fill (accent, inverse, destructive) or tinted status color (info, success, warning, error). A count badge is inverse and an entitlement badge info unless set. brand still works as a deprecated alias of accent.
sizesm | md | lg | xlprovider / lgHeight in fixed steps: sm 16, md 20, lg 24, xl 28 px. Type and icon follow the size.
shapedefault | pilldefaultpill rounds the badge fully at every size. default takes the theme's badge radius, --sp-rw-radius-badge.
profilecount | entitlement | status | verifiedunsetcount is a compact notification count, 16 px high with a full radius and 10 px type, solid unless a variant is set; it grows as a pill and never truncates. IconButton uses it. entitlement labels a plan or an entitlement, such as Business, with its icon in a 16 px box before the text; it takes the info tint unless a variant is set and draws no contour. status is a state in words after a colored dot, without fill or contour; the variant gives the dot its tone. verified is a solid accent circle with a ring in the surface color, placed on an avatar. It sizes the check icon you pass and needs an accessible name, such as visually hidden text.
lightbooleanfalseRuns two soft lights around an opaque badge for the one announcement on a page. Reduced motion keeps them still.
renderReactElement | render functionspanCompose onto a link or another element.
classNamestringunsetAdditional classes.
childrenReactNodeunsetVisible label or count. Text children become one label, which ends with an ellipsis where the badge meets the edge of its space and carries its full text as its title; icons and other elements stay beside it.

BadgeProps, BadgeProfile and BadgeVariant describe these contracts; BadgeDeprecatedVariant holds the earlier brand. badgeVariants({ size, variant, className }) returns the utility classes Badge adds beside its stylesheet, with lg size and default variant; the look comes from the kit stylesheet through the attributes Badge renders, and the helper does not resolve provider size. Remaining attributes and refs follow the rendered element. Composition contract.

Read the installed source.

Usage notes

A status update is not automatically announced. Put changing application feedback in an appropriate live region. A count needs a contextual accessible name; decorative icons should be hidden from assistive technology.