Badge

A compact inline label for status, category or metadata, with an optional icon, avatar and close button.

Installation

bash
npx @smicolon/fasla-ui@latest init
npx @smicolon/fasla-ui@latest add badge

Preview

NewPaidFeatured

Variants

variant sets the visual weight and tone the meaning. Solid is the loudest; Soft lays a tint over the card surface, so it reads the same wherever it sits; Outline is the quietest.

NewDraftIn reviewPaidAwaiting paymentOverdue
NewDraftIn reviewPaidAwaiting paymentOverdue
NewDraftIn reviewPaidAwaiting paymentOverdue

radius="standard" swaps the pill for the design system’s small radius.

NewNewNewNew

Sizes

Heights are minimums: 20, 22 and 26px in English. Arabic’s taller line grows them to 24, 24 and 28px rather than squeezing the text.

FeaturedFeaturedFeatured

States

Pass onClose and the badge gets a real close button. Tab to one to see the focus ring, which wraps the whole badge 2px out.

DesignEngineeringResearchMarketing
FeaturedLaylaLaylaReviewer

Accessibility

  • The badge is a plain <span>. Only the close button is interactive, and it is a native <button>.
  • The close button is named after the label, "Remove Cairo", or "إزالة القاهرة" when the nearest lang is Arabic. It follows a language change without a reload. Pass closeLabel for more context, such as "Remove filter: Cairo".
  • The icon is decorative and hidden from assistive technology. The avatar is not: give the Avatar a name, or name="" when the label already names the person.
  • Don’t rely on tone alone to carry meaning — the label should say it.
  • Direction is inherited from dir, never a prop, so the badge can never disagree with the page.

Props

PropTypeDefaultDescription
childrenReactNode—The label.
variant"solid" | "soft" | "outline""solid"Visual weight.
tone"primary" | "secondary" | "info" | "success" | "warning" | "destructive""primary"Meaning, shown as colour.
size"sm" | "md" | "lg""sm"Badge size.
radius"rounded" | "standard""rounded"Pill or small radius.
iconReactNode—Icon before the label.
avatarReactNode—Avatar before the label.
onClose(event) => void—Adds a close button; called when pressed.
closeLabelstring—Close button’s accessible name, in the page language by default.
classNamestring—Extra classes for the component.

Usage

Variants and tones

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

export function OrderBadges() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Badge>New</Badge>
      <Badge variant="soft" tone="success">Paid</Badge>
      <Badge variant="outline" tone="destructive" size="md">Overdue</Badge>
    </div>
  )
}

Icon and avatar — the badge sizes and colours them

tsx
import { Star } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Avatar } from "@/components/ui/avatar"

interface Reviewer {
  name: string
  // The photo from your own data. Without one, the avatar shows the user icon.
  photo?: string
}

const reviewer: Reviewer = { name: "Layla" }

export function ReviewerBadges() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Badge icon={<Star />}>Featured</Badge>
      <Badge avatar={<Avatar size="12" radius="rounded" src={reviewer.photo} name="" />}>
        {reviewer.name}
      </Badge>
    </div>
  )
}

Removable

tsx
"use client"

import { useState } from "react"
import { Badge } from "@/components/ui/badge"

export function FilterBadges() {
  const [filters, setFilters] = useState(["Design", "Engineering", "Research"])
  const remove = (filter: string) => setFilters((current) => current.filter((f) => f !== filter))

  return (
    <div className="flex flex-wrap items-center gap-2">
      {filters.map((filter) => (
        <Badge
          key={filter}
          variant="soft"
          onClose={() => remove(filter)}
          closeLabel={`Remove filter: ${filter}`}
        >
          {filter}
        </Badge>
      ))}
    </div>
  )
}