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 badgePreview
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
langis Arabic. It follows a language change without a reload. PasscloseLabelfor 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, orname=""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
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | 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. |
icon | ReactNode | — | Icon before the label. |
avatar | ReactNode | — | Avatar before the label. |
onClose | (event) => void | — | Adds a close button; called when pressed. |
closeLabel | string | — | Close button’s accessible name, in the page language by default. |
className | string | — | 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>
)
}