# Badge

Displays a badge or a component that looks like a badge.

```tsx
import { Badge } from "@/components/ui/badge"
import { HStack } from "@/components/ui/stack"

export default function BadgeDemo() {
  return (
    <HStack align="center" gap={2} wrap="wrap">
      <Badge>Badge</Badge>
      <Badge variant="secondary">Secondary</Badge>
      <Badge variant="destructive">Destructive</Badge>
      <Badge variant="outline">Outline</Badge>
    </HStack>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` from the same page.
   The badge's focus ring and invalid ring come from it. The badge also needs
   `interop.css`, which sizes the icon.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/badge.tsx"
   "use client"

   import { useRender } from "@base-ui/react/use-render"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import { states } from "@/theme/states"
   import {
     color,
     duration,
     ease,
     leading,
     radius,
     shadow,
     space,
     text,
   } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     base: {
       display: "inline-flex",
       alignItems: "center",
       justifyContent: "center",
       width: "fit-content",
       flexShrink: 0,
       whiteSpace: "nowrap",
       overflow: "hidden",
       height: space.s5,
       gap: space.s1,
       paddingInline: space.s2,
       paddingBlock: space.s0_5,
       borderRadius: radius.xl4,
       borderWidth: 1,
       borderStyle: "solid",
       fontSize: text.xs,
       lineHeight: leading.xs,
       fontWeight: 500,
       transitionProperty: "color, background-color, border-color, box-shadow",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
       "--carbon-icon-size": space.s3,
     },
     iconStart: { paddingInlineStart: space.s1_5 },
     iconEnd: { paddingInlineEnd: space.s1_5 },
   })

   const variants = stylex.create({
     default: {
       backgroundColor: color.primary,
       color: color.primaryForeground,
     },
     secondary: {
       backgroundColor: color.secondary,
       color: color.secondaryForeground,
     },
     destructive: {
       backgroundColor: color.buttonDestructiveBg,
       color: color.destructive,
       boxShadow: { default: null, ":focus-visible": shadow.ringInvalid },
     },
     outline: {
       "--carbon-border-color": color.border,
       color: color.foreground,
     },
     ghost: {},
     link: {
       color: color.primary,
       textUnderlineOffset: "4px",
       textDecorationLine: "none",
     },
   })

   const badgeStyles = {
     base: [styles.base, states.focusRing],
     variants,
     iconStart: styles.iconStart,
     iconEnd: styles.iconEnd,
   }

   type BadgeVariant = keyof typeof variants

   type BadgeProps = useRender.ComponentProps<"span"> & {
     variant?: BadgeVariant
     iconPosition?: "start" | "end" | "both"
     xstyle?: StyleXStyles
   }

   function Badge({
     variant = "default",
     iconPosition,
     xstyle,
     className,
     render,
     ...props
   }: BadgeProps) {
     const sx = stylex.props(
       styles.base,
       states.focusRing,
       variants[variant] ?? variants.default,
       (iconPosition === "start" || iconPosition === "both") && styles.iconStart,
       (iconPosition === "end" || iconPosition === "both") && styles.iconEnd,
       props["aria-invalid"] && states.invalidRing,
       xstyle,
     )

     return useRender({
       defaultTagName: "span",
       render,
       props: {
         "data-slot": "badge",
         "data-variant": variant,
         ...sx,
         ...props,
         className: clsx("carbon-icons", sx.className, className),
       },
     })
   }

   export { Badge, badgeStyles, type BadgeVariant }
   ```

4. Update the import paths to match your project setup.

## Usage

```tsx
import { Badge } from "@/components/ui/badge"
```

```tsx
<Badge variant="secondary">Badge</Badge>
```

## Variants

Use the `variant` prop to change the visual style.

```tsx
import { Badge } from "@/components/ui/badge"
import { HStack } from "@/components/ui/stack"

export default function BadgeVariants() {
  return (
    <HStack align="center" gap={2} wrap="wrap">
      <Badge variant="default">Default</Badge>
      <Badge variant="secondary">Secondary</Badge>
      <Badge variant="destructive">Destructive</Badge>
      <Badge variant="outline">Outline</Badge>
      <Badge variant="ghost">Ghost</Badge>
      <Badge variant="link">Link</Badge>
    </HStack>
  )
}
```

## With icon

Set `iconPosition` to the side the icon is on, so the badge tightens its
padding there.

```tsx
import { Badge } from "@/components/ui/badge"
import { HStack } from "@/components/ui/stack"
import { BadgeCheck, Bookmark } from "lucide-react"

export default function BadgeWithIcon() {
  return (
    <HStack align="center" gap={2} wrap="wrap">
      <Badge variant="secondary" iconPosition="start">
        <BadgeCheck />
        Verified
      </Badge>
      <Badge variant="outline" iconPosition="end">
        Bookmark
        <Bookmark />
      </Badge>
    </HStack>
  )
}
```

> **Note**
>
> Without `iconPosition` the padding stays symmetric and the glyph looks inset
> against the round end. StyleX has no `:has()`, so the badge has to be told
> which side the icon is on.

## Loading

Render a [Spinner](https://carbon.style/docs/components/spinner) and set `iconPosition` to its
side.

```tsx
import { Badge } from "@/components/ui/badge"
import { Spinner } from "@/components/ui/spinner"
import { HStack } from "@/components/ui/stack"

export default function BadgeLoading() {
  return (
    <HStack align="center" gap={2} wrap="wrap">
      <Badge variant="destructive" iconPosition="start">
        <Spinner aria-hidden="true" />
        Deleting
      </Badge>
      <Badge variant="secondary" iconPosition="end">
        Generating
        <Spinner aria-hidden="true" />
      </Badge>
    </HStack>
  )
}
```

## Link

Pass `render` to change the element the badge renders.

```tsx
import * as stylex from "@stylexjs/stylex"
import { color } from "@/theme/tokens.stylex"
import { Badge } from "@/components/ui/badge"
import { HStack } from "@/components/ui/stack"
import { ArrowUpRight } from "lucide-react"

const styles = stylex.create({
  primary: {
    backgroundColor: {
      default: color.primary,
      ":hover": `color-mix(in oklab, ${color.primary} 80%, transparent)`,
    },
  },
  outline: { backgroundColor: { default: null, ":hover": color.muted } },
})

export default function BadgeAsLink() {
  return (
    <HStack align="center" gap={2} wrap="wrap">
      <Badge
        xstyle={styles.primary}
        render={<a href="https://base-ui.com" target="_blank" rel="noopener noreferrer" />}
      >
        Base UI
      </Badge>
      <Badge
        variant="outline"
        xstyle={styles.outline}
        iconPosition="end"
        render={<a href="https://base-ui.com/react/utils/use-render" />}
      >
        useRender
        <ArrowUpRight />
      </Badge>
    </HStack>
  )
}
```

Badges have no built-in hover styles. Add a hover through `xstyle` when using
`render` for a link or another interactive element.

## Custom colors

Pass `xstyle` to change the background, border and text colors.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Badge } from "@/components/ui/badge"
import { HStack } from "@/components/ui/stack"
import { color } from "@/theme/tokens.stylex"

const styles = stylex.create({
  info: {
    backgroundColor: color.calloutInfoBg,
    borderColor: color.calloutInfoBorder,
    color: color.calloutInfoAccent,
  },
  success: {
    backgroundColor: color.calloutSuccessBg,
    borderColor: color.calloutSuccessBorder,
    color: color.calloutSuccessAccent,
  },
  warn: {
    backgroundColor: color.calloutWarnBg,
    borderColor: color.calloutWarnBorder,
    color: color.calloutWarnAccent,
  },
  tip: {
    backgroundColor: color.calloutTipBg,
    borderColor: color.calloutTipBorder,
    color: color.calloutTipAccent,
  },
})

export default function BadgeColors() {
  return (
    <HStack align="center" gap={2} wrap="wrap">
      <Badge xstyle={styles.info}>Info</Badge>
      <Badge xstyle={styles.success}>Success</Badge>
      <Badge xstyle={styles.warn}>Warning</Badge>
      <Badge xstyle={styles.tip}>Tip</Badge>
    </HStack>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

### Badge

Renders a `span` through Base UI's
[`useRender`](https://base-ui.com/react/utils/use-render), so it accepts
`render` alongside every `span` attribute.

| Prop           | Type                                                                          | Default     |
| -------------- | ----------------------------------------------------------------------------- | ----------- |
| `variant`      | `"default" \| "secondary" \| "destructive" \| "outline" \| "ghost" \| "link"` | `"default"` |
| `iconPosition` | `"start" \| "end" \| "both"`                                                  | —           |
| `render`       | `ReactElement \| ((props, state) => ReactElement)`                            | `<span />`  |
| `xstyle`       | `StyleXStyles`                                                                | —           |
| `className`    | `string`                                                                      | —           |

`xstyle` is merged last, so it beats the variant. `className` is concatenated
with no conflict resolution. Use it for plain-CSS hooks.

Setting `aria-invalid` switches the border and ring to the destructive pair.

### badgeStyles

The style maps behind `Badge`. Spread them onto any element:

```tsx
import * as stylex from "@stylexjs/stylex"
import { badgeStyles } from "@/components/ui/badge"
import { clsx } from "clsx"

const sx = stylex.props(badgeStyles.base, badgeStyles.variants.outline)

;<span data-variant="outline" {...sx} className={clsx("carbon-icons", sx.className)} />
```

Merge `className` from the `stylex.props()` result by hand. A second
`className` attribute beside the spread replaces it.

| Key         | What it is                                 |
| ----------- | ------------------------------------------ |
| `base`      | Shape and type, with the shared focus ring |
| `variants`  | One namespace per `variant`                |
| `iconStart` | Leading-icon padding                       |
| `iconEnd`   | Trailing-icon padding                      |

`base` is two styles, the badge's own and the shared `states.focusRing`. Set a
border of your own through `--carbon-border-color` rather than `borderColor`. A
flat `borderColor` replaces the property with all of its conditions, so the ring loses its focus colour.

Keep the `carbon-icons` class. `interop.css` sizes icons off it, and without it
a Lucide icon keeps its own 24px.

### Data attributes

| Attribute           | On        | Effect                                                 |
| ------------------- | --------- | ------------------------------------------------------ |
| `data-slot="badge"` | the badge | Targets the badge from a parent                        |
| `data-variant`      | the badge | Reflects the `variant` prop, for styling from a parent |

### CSS variables

| Variable                | On        | Effect                                                            |
| ----------------------- | --------- | ----------------------------------------------------------------- |
| `--carbon-icon-size`    | the badge | The icon's size, read by `interop.css`. Defaults to 12px          |
| `--carbon-border-color` | the badge | The resting border colour the focus ring reads. `outline` sets it |
