MC Badge
A small label for status or categorization.
Overview
The mc-badge is a compact label used to highlight status, categories, or metadata.
It is built on @base-ui/react and styled with Tailwind CSS v4. The same file also exports McBadgeGroup, a pill that pairs a badge with a short message.
Preview
import { McBadge } from '@/components/ui/mc-badge';export default function McBadgeDemo() { return <McBadge>Label</McBadge>;}Add to Your Project
Deploy the mc-badge directly to your components/ui directory:
Manual Setup
If you prefer manual control:
'use client';import { mergeProps } from '@base-ui/react/merge-props';import { useRender } from '@base-ui/react/use-render';import { cva, type VariantProps } from 'class-variance-authority';import { createContext, useContext, type ComponentProps, type ReactNode } from 'react';import { cn } from '@/lib/utils';const McBadgeVariants = cva( 'group/badge inline-flex shrink-0 items-center justify-center overflow-hidden rounded-[16px] border border-border font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:size-3 [&>svg]:shrink-0', { variants: { variant: { default: 'bg-primary-foreground text-primary [a]:hover:bg-primary-foreground/80', secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80', destructive: 'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20', outline: 'text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground', ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50', primary: 'bg-accent-foreground text-accent', }, size: { sm: 'paragraph-xs px-[7px] py-px', md: 'paragraph-sm px-[9px] py-px', lg: 'paragraph-md px-[11px] py-[3px]', groupLeadingLg: 'paragraph-md px-[9px] py-px', }, }, defaultVariants: { variant: 'default', size: 'sm', }, });type BadgeSize = 'sm' | 'md' | 'lg';type BadgeKind = | 'dot-start' | 'dot-end' | 'image-start' | 'image-end' | 'icon-start' | 'icon-end' | 'only';// Figma paddings minus the 1px border, because Figma strokes sit inside the box.const McBadgePadding: Record<BadgeSize, Record<BadgeKind, string>> = { sm: { 'dot-start': 'gap-1.5 pl-[5px] pr-[7px]', 'dot-end': 'gap-1.5 pl-[7px] pr-[5px]', 'image-start': 'gap-1.5 pl-[2px] pr-[7px]', 'image-end': 'gap-1.5 pl-[7px] pr-[2px]', 'icon-start': 'gap-1 pl-[5px] pr-[7px]', 'icon-end': 'gap-1 pl-[7px] pr-[5px]', only: 'p-[3px]', }, md: { 'dot-start': 'gap-1.5 pl-[7px] pr-[9px]', 'dot-end': 'gap-1.5 pl-[9px] pr-[7px]', 'image-start': 'gap-1.5 pl-[3px] pr-[9px]', 'image-end': 'gap-1.5 pl-[9px] pr-[3px]', 'icon-start': 'gap-1 pl-[9px] pr-[7px]', 'icon-end': 'gap-1 pl-[9px] pr-[7px]', only: 'p-[5px]', }, lg: { 'dot-start': 'gap-1.5 pl-[9px] pr-[11px]', 'dot-end': 'gap-1.5 pl-[11px] pr-[9px]', 'image-start': 'gap-1.5 pl-[5px] pr-[11px]', 'image-end': 'gap-1.5 pl-[11px] pr-[5px]', 'icon-start': 'gap-1 pl-[11px] pr-[9px]', 'icon-end': 'gap-1 pl-[11px] pr-[9px]', only: 'p-[7px]', },};type McBadgeGroupContextValue = { size: 'md' | 'lg'; badgePosition: 'leading' | 'trailing';};const McBadgeGroupContext = createContext<McBadgeGroupContextValue | null>(null);function McBadgeDot({ className, ...props }: ComponentProps<'span'>) { return ( <span data-slot="badge-dot" aria-hidden="true" className={cn('inline-block size-2 shrink-0 rounded-full bg-current', className)} {...props} /> );}function McBadge({ className, variant, size = 'sm', render, icon, iconPosition = 'start', iconOnly = false, image, imageAlt = '', imagePosition = 'start', children, ...props}: useRender.ComponentProps<'span'> & Omit<VariantProps<typeof McBadgeVariants>, 'size'> & { size?: BadgeSize; icon?: ReactNode; iconPosition?: 'start' | 'end'; iconOnly?: boolean; image?: string; imageAlt?: string; imagePosition?: 'start' | 'end'; }) { const group = useContext(McBadgeGroupContext); const resolvedVariant = variant ?? (group ? 'primary' : 'default'); const resolvedSize: BadgeSize = group ? (group.size === 'md' ? 'sm' : 'md') : size; const variantSize = group?.size === 'lg' && group.badgePosition === 'leading' ? 'groupLeadingLg' : resolvedSize; const isDot = isDotIcon(icon); const placement = iconPosition === 'end' ? 'end' : 'start'; const kind: BadgeKind | undefined = image ? `image-${imagePosition}` : icon ? iconOnly ? 'only' : `${isDot ? 'dot' : 'icon'}-${placement}` : undefined; const iconEl = icon ? ( isDot ? ( icon ) : ( <span data-slot="badge-icon" className="inline-flex size-3 shrink-0 items-center justify-center [&>svg]:size-full" > {icon} </span> ) ) : null; const imageEl = image ? ( <img data-slot="badge-image" src={image} alt={imageAlt} className="size-4 shrink-0 rounded-full object-cover" /> ) : null; const leading = !image && placement === 'start' ? iconEl : null; const trailing = !image && placement === 'end' ? iconEl : null; const dataProps: Record<string, string> = { 'data-slot': 'badge' }; if (icon && !image) dataProps['data-icon'] = `inline-${placement}`; return useRender({ defaultTagName: 'span', props: mergeProps<'span'>( { className: cn( McBadgeVariants({ variant: resolvedVariant, size: variantSize }), kind && McBadgePadding[resolvedSize][kind], className ), ...dataProps, children: ( <> {imagePosition === 'start' ? imageEl : null} {leading} {iconOnly && icon ? null : children} {trailing} {imagePosition === 'end' ? imageEl : null} </> ), }, props ), render, state: { slot: 'badge', variant: resolvedVariant, size: resolvedSize, }, });}function isDotIcon(icon: ReactNode) { return ( typeof icon === 'object' && icon !== null && 'type' in icon && (icon as { type: unknown }).type === McBadgeDot );}const McBadgeGroupVariants = cva( 'inline-flex shrink-0 items-center rounded-[16px] border border-border bg-primary-foreground py-[3px] font-medium whitespace-nowrap text-primary', { variants: { size: { md: 'paragraph-xs gap-2', lg: 'paragraph-md gap-3', }, badgePosition: { leading: 'pl-[3px] pr-[11px] has-[[data-slot=badge-group-icon]]:pr-[9px]', trailing: '', }, }, compoundVariants: [ { size: 'md', badgePosition: 'trailing', className: 'pl-[11px] pr-[3px]' }, { size: 'lg', badgePosition: 'trailing', className: 'pl-[13px] pr-[5px] has-[[data-icon=inline-end]]:pr-[3px]', }, ], defaultVariants: { size: 'md', badgePosition: 'leading', }, });function McBadgeGroup({ className, size = 'md', badgePosition = 'leading', render, ...props}: useRender.ComponentProps<'span'> & { size?: 'md' | 'lg'; badgePosition?: 'leading' | 'trailing';}) { const element = useRender({ defaultTagName: 'span', props: mergeProps<'span'>( { className: cn(McBadgeGroupVariants({ size, badgePosition }), className), ...({ 'data-slot': 'badge-group' } as Record<string, string>), }, props ), render, state: { slot: 'badge-group', size, badgePosition, }, }); return ( <McBadgeGroupContext.Provider value={{ size, badgePosition }}> {element} </McBadgeGroupContext.Provider> );}function McBadgeGroupText({ className, icon, children, ...props}: ComponentProps<'span'> & { icon?: ReactNode }) { return ( <span data-slot="badge-group-text" className={cn('inline-flex items-center gap-1', className)} {...props} > {children} {icon ? ( <span data-slot="badge-group-icon" className="inline-flex size-4 shrink-0 items-center justify-center [&>svg]:size-full" > {icon} </span> ) : null} </span> );}export { McBadge, McBadgeDot, McBadgeGroup, McBadgeGroupText, McBadgeVariants, McBadgeGroupVariants,};Ensure your @/lib/utils or equivalent classname helper is correctly imported.
Usage
import { McBadge } from '@/components/ui/mc-badge';
export default function Example() {
return <McBadge>Label</McBadge>;
}Examples
Variants
default is the Figma Gray badge, and primary is the badge used inside a group. The other variants are extras.
import { McBadge } from '@/components/ui/mc-badge';export default function McBadgeVariantsDemo() { return ( <div className="flex flex-wrap items-center gap-3"> <McBadge variant="default">Default</McBadge> <McBadge variant="primary">Primary</McBadge> <McBadge variant="secondary">Secondary</McBadge> <McBadge variant="destructive">Destructive</McBadge> <McBadge variant="outline">Outline</McBadge> <McBadge variant="ghost">Ghost</McBadge> </div> );}Sizes
import { McBadge } from '@/components/ui/mc-badge';export default function McBadgeSizesDemo() { return ( <div className="flex flex-wrap items-center gap-3"> <McBadge size="sm">Small</McBadge> <McBadge size="md">Medium</McBadge> <McBadge size="lg">Large</McBadge> </div> );}Icons
Pass any icon with icon. Use McBadgeDot for the dot, iconPosition="end" for a trailing icon, and iconOnly for a badge with no label.
import { ArrowRight, ArrowUp, Plus, X } from 'lucide-react';import { McBadge, McBadgeDot } from '@/components/ui/mc-badge';export default function McBadgeIconsDemo() { return ( <div className="flex flex-wrap items-center gap-3"> <McBadge icon={<McBadgeDot />}>Dot</McBadge> <McBadge icon={<ArrowUp />}>Icon left</McBadge> <McBadge icon={<ArrowRight />} iconPosition="end"> Icon right </McBadge> <McBadge icon={<X />} iconPosition="end"> Close </McBadge> <McBadge icon={<Plus />} iconOnly aria-label="Add" /> </div> );}Image
Use image for a country flag or an avatar. When image is set, icon is ignored.
import { McBadge } from '@/components/ui/mc-badge';export default function McBadgeImageDemo() { return ( <McBadge image="https://flagcdn.com/w40/au.png" imageAlt="Australia"> Australia </McBadge> );}Badge group
McBadgeGroup sizes its inner McBadge for you. Put the badge first for a leading badge, and pass icon to McBadgeGroupText for the arrow.
import { ArrowRight } from 'lucide-react';import { McBadge, McBadgeGroup, McBadgeGroupText } from '@/components/ui/mc-badge';export default function McBadgeGroupDemo() { return ( <McBadgeGroup> <McBadge>New feature</McBadge> <McBadgeGroupText icon={<ArrowRight />}>We’ve just released a new feature</McBadgeGroupText> </McBadgeGroup> );}Trailing badge
Set badgePosition="trailing" and put the badge last. The arrow goes in the badge, with iconPosition="end".
import { ArrowRight } from 'lucide-react';import { McBadge, McBadgeGroup, McBadgeGroupText } from '@/components/ui/mc-badge';export default function McBadgeGroupTrailingDemo() { return ( <McBadgeGroup badgePosition="trailing"> <McBadgeGroupText>We’ve just released a new feature</McBadgeGroupText> <McBadge icon={<ArrowRight />} iconPosition="end"> New feature </McBadge> </McBadgeGroup> );}Group sizes
import { McBadge, McBadgeGroup, McBadgeGroupText } from '@/components/ui/mc-badge';export default function McBadgeGroupSizesDemo() { return ( <div className="flex flex-col items-start gap-3"> <McBadgeGroup size="md"> <McBadge>New feature</McBadge> <McBadgeGroupText>We’ve just released a new feature</McBadgeGroupText> </McBadgeGroup> <McBadgeGroup size="lg"> <McBadge>New feature</McBadge> <McBadgeGroupText>We’ve just released a new feature</McBadgeGroupText> </McBadgeGroup> </div> );}As a link
Use the render prop to render the badge as another element.
<McBadge render={<a href="/changelog" />}>Changelog</McBadge>Accessibility
- A badge has no role and is not focusable. Keep its label short and meaningful on its own.
- A badge with
iconOnlyhas no text. Passaria-labelso screen readers can announce it. McBadgeDotis hidden from assistive technology. Do not use color alone to carry meaning, write the status in the label.- To make a badge dismissible, put a real
<button>with anaria-labelin theiconslot.
API Reference
McBadge
Extends the Base UI useRender span props.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "primary" | "secondary" | "destructive" | "outline" | "ghost" | "default" | Visual style. Inside McBadgeGroup the default is "primary". |
size | "sm" | "md" | "lg" | "sm" | Size of the badge. Inside McBadgeGroup the group sets it. |
icon | ReactNode | undefined | Icon to show. Use McBadgeDot for the dot. |
iconPosition | "start" | "end" | "start" | Side of the label where the icon sits. |
iconOnly | boolean | false | Hides the label and shows only icon. Add aria-label. |
image | string | undefined | URL of a flag or avatar image. Replaces icon. |
imageAlt | string | "" | Alt text for image. |
imagePosition | "start" | "end" | "start" | Side of the label where the image sits. |
render | ReactElement | ((props: HTMLProps) => ReactElement) | undefined | Polymorphic render prop from Base UI. |
className | string | undefined | Additional CSS classes. |
McBadgeGroup
| Prop | Type | Default | Description |
|---|---|---|---|
size | "md" | "lg" | "md" | Size of the group and of the inner badge. |
badgePosition | "leading" | "trailing" | "leading" | Whether the badge comes before or after the message. |
render | ReactElement | ((props: HTMLProps) => ReactElement) | undefined | Polymorphic render prop from Base UI. |
className | string | undefined | Additional CSS classes. |
McBadgeGroupText
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | undefined | Icon shown after the message, for a leading badge. |
className | string | undefined | Additional CSS classes. |
McBadgeDot
An 8px dot that follows the text color. Pass it as the icon of McBadge.
Other exports
McBadgeVariants and McBadgeGroupVariants are the cva style functions, for styling your own elements like a badge.
Australia