Badge
A small label for a status, count or category.
Preview
Invoices
- $1,284.50
Ines Duarte
INV-1042
- $640.00
Malik Osei
INV-1041
- $2,310.75
Priya Nair
INV-1039
- $96.20
Tomás Reyes
INV-1043
Install
Add the editable source with the shadcn CLI. Your project also needs the shared theme tokens this component uses. Setup and requirements. The Pebble installer is not published yet.
npx shadcn@latest add https://pebbleui.com/r/badge.jsonProps
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "default" | "secondary" | "outline" | "accent" | "success" | "warning" | "danger" | "default" | Visual style. default is a small lit key, accent is ember, status variants are tinted. |
| size | "sm" | "md" | "md" | Height and text size. |
| dot | boolean | false | Shows a small status dot before the label, coloured by the variant. |
| asChild | boolean | false | Renders the single child (e.g. a link) with badge styles. |
Build notes
A styled `span` (or any element via `asChild`, using Radix `Slot`). Variants are a lookup of token classes; status variants use a 15% tint of the status color, a 30% border and the `*-strong` text token so small text keeps contrast on the tint in both themes. `data-variant` is exposed for styling. Hover dimming applies only when the badge is rendered as a link or button (`[a&]:hover`, `[button&]:hover`), so static badges do not look clickable. **Pebble (2026-11-03).** Badges became small objects. `default` is a miniature lit key (key gradient, key-edge ring, key shine); `secondary` a raised card pebble (hairline ring, `shadow-raised-xs`, top highlight); `outline` a plain border ring. Status variants keep the `*-strong` text on a 12-15% tint but swap the border for a 30% ring, and a new `accent` (ember) variant marks the new or live item. The new `dot` prop renders an `aria-hidden` dot before the label in the variant's solid colour; with `asChild` the children go through `Slot.Slottable` so the dot lands inside the child element. The new `size` prop adds `sm` (20 px, 11 px text) beside the default `md` (24 px). Interactive badges get a brightness hover and a 0.97 press that reduced motion removes. `tabular-nums` keeps counts from jittering. <!-- PEBBLE-LICENSE -->
View source
registry/data/badge/badge.tsx
/* PEBBLE-LICENSE
Built by Pebble. Component: badge.
First-party license: MIT
The following first-party terms apply only to original Pebble material.
MIT License
Copyright (c) 2026 Praxys Solutions LLC (Pebble)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
END PEBBLE-LICENSE */
import * as React from "react";
import { Slot } from "radix-ui";
import { cn } from "@/lib/utils";
// Pebble: badges are small objects, not flat chips. `default` is a miniature
// lit key, `secondary` a raised card pebble, `outline` a hairline ring. Status
// variants are tinted wells with a strong-text label and a matching dot, and
// `accent` (ember) is reserved for the new or live thing.
const variants = {
default:
"bg-linear-to-b from-key-top to-key-bottom text-primary-foreground ring-1 ring-key-edge shadow-[var(--key-shine)]",
secondary: "bg-card text-foreground ring-1 ring-hairline shadow-raised-xs inset-shadow-highlight",
outline: "bg-transparent text-foreground ring-1 ring-border",
accent: "bg-accent/12 text-accent-strong ring-1 ring-accent/30",
success: "bg-success/12 text-success-strong ring-1 ring-success/30",
warning: "bg-warning/15 text-warning-strong ring-1 ring-warning/35",
danger: "bg-danger/12 text-danger-strong ring-1 ring-danger/30",
} as const;
const dots = {
default: "bg-primary-foreground/80",
secondary: "bg-muted-foreground",
outline: "bg-muted-foreground",
accent: "bg-accent",
success: "bg-success",
warning: "bg-warning",
danger: "bg-danger",
} as const;
const sizes = {
sm: "h-5 gap-1 px-1.5 text-[11px]",
md: "h-6 gap-1.5 px-2 text-xs",
} as const;
export type BadgeProps = React.ComponentProps<"span"> & {
variant?: keyof typeof variants;
size?: keyof typeof sizes;
/** Shows a small status dot before the label, coloured by the variant. */
dot?: boolean;
asChild?: boolean;
};
export function Badge({
variant = "default",
size = "md",
dot = false,
asChild = false,
className,
children,
...props
}: BadgeProps) {
const Comp = asChild ? Slot.Root : "span";
const marker = dot ? (
<span
aria-hidden
data-slot="badge-dot"
className={cn("size-1.5 shrink-0 rounded-full", dots[variant])}
/>
) : null;
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(
"inline-flex w-fit shrink-0 items-center rounded-full font-medium whitespace-nowrap tabular-nums select-none [&_svg]:size-3 [&_svg]:shrink-0",
"transition-[background-color,box-shadow,filter,scale] duration-(--motion-fast) ease-enter motion-reduce:transition-none",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
// Interactive badges (links, buttons) get a whisper of hover and a felt press.
"[a&]:cursor-pointer [a&]:hover:brightness-[0.97] dark:[a&]:hover:brightness-110 [button&]:cursor-pointer [button&]:hover:brightness-[0.97] dark:[button&]:hover:brightness-110",
"[a&]:active:scale-[0.97] motion-reduce:[a&]:active:scale-100 [button&]:active:scale-[0.97] motion-reduce:[button&]:active:scale-100",
variants[variant],
sizes[size],
className,
)}
{...props}
>
{marker}
{asChild ? <Slot.Slottable>{children}</Slot.Slottable> : children}
</Comp>
);
}
