UIData Displaymotion/react

Badge

A compact status badge in six variants (default, secondary, outline, success, warning, destructive) that springs in on mount, with an optional pulsing status dot and a remove button that animates the badge out.

Preview

In review

Customize

9 props

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add badge

Writes components/ui/badge.tsx and components/styles/badge.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { Badge } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <Badge />6  );7}

Props

PropTypeDefault
label

Badge text (ignored when children are provided).

string'In review'
children

Custom badge content.

React.ReactNodeundefined
variant

Visual variant.

'default' | 'secondary' | 'outline' | 'success' | 'warning' | 'destructive''default'
size

Height, padding and font-size preset.

'sm' | 'md' | 'lg''md'
shape

Fully rounded pill or a 6px rounded rectangle.

'pill' | 'rounded''pill'
dot

Show a leading status dot.

booleantrue
pulse

Animate the dot with a radiating pulse.

booleantrue
removable

Show a remove button; the badge animates out when pressed.

booleantrue
onRemove

Called after the remove button is pressed.

() => voidundefined
accentColor

Fill of the default variant and dot color of secondary/outline.

string'#6366f1'
animateIn

Pop in with a small spring on mount.

booleantrue
className

Additional class merged onto the root element.

string''
style

Inline styles merged onto the root element.

React.CSSPropertiesundefined

Accessibility

Renders an inline span whose text is the accessible content; the status dot is aria-hidden decoration. The optional remove control is a native button labelled 'Remove <label>' with a visible focus ring. Under prefers-reduced-motion the pop-in, exit scale and dot pulse are disabled.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/badge.tsx
1"use client";2 3import * as React from "react";4import { AnimatePresence, motion, useReducedMotion } from "motion/react";5import styles from "../styles/badge.module.css";6 7export interface BadgeProps {8  /** Badge text (ignored when children are provided) */9  label?: string;10  /** Custom badge content */11  children?: React.ReactNode;12  /** Visual variant */13  variant?: "default" | "secondary" | "outline" | "success" | "warning" | "destructive";14  /** Size preset */15  size?: "sm" | "md" | "lg";16  /** Corner style */17  shape?: "pill" | "rounded";18  /** Show a leading status dot */19  dot?: boolean;20  /** Animate the dot with a soft radiating pulse */21  pulse?: boolean;22  /** Show a remove (x) button; the badge animates out when pressed */23  removable?: boolean;24  /** Called after the remove button is pressed */25  onRemove?: () => void;26  /** Fill color of the default variant */27  accentColor?: string;28  /** Pop in with a small spring on mount */29  animateIn?: boolean;30  /** Optional className merged onto the root */31  className?: string;32  /** Optional inline styles merged onto the root */33  style?: React.CSSProperties;34}35 36export function Badge({37  label = "In review",38  children,39  variant = "default",40  size = "md",41  shape = "pill",42  dot = true,43  pulse = true,44  removable = true,45  onRemove,46  accentColor = "#6366f1",47  animateIn = true,48  className = "",49  style,50}: BadgeProps) {51  const reduce = useReducedMotion();52  const [visible, setVisible] = React.useState(true);53  const text = typeof children === "string" ? children : label;54 55  const remove = () => {56    setVisible(false);57    onRemove?.();58  };59 60  return (61    <AnimatePresence initial={animateIn && !reduce}>62      {visible && (63        <motion.span64          key="badge"65          className={`${styles.root} ${className}`.trim()}66          data-variant={variant}67          data-size={size}68          data-shape={shape}69          style={{ "--mf-bd-accent": accentColor, ...style } as React.CSSProperties}70          initial={{ opacity: 0, scale: 0.8, y: 2 }}71          animate={{ opacity: 1, scale: 1, y: 0 }}72          exit={{ opacity: 0, scale: reduce ? 1 : 0.8 }}73          transition={reduce ? { duration: 0.1 } : { type: "spring", stiffness: 520, damping: 30 }}74        >75          {dot && (76            <span aria-hidden="true" className={styles.dot} data-pulse={pulse && !reduce ? "" : undefined} />77          )}78          <span className={styles.label}>{children ?? label}</span>79          {removable && (80            <button type="button" className={styles.remove} aria-label={`Remove ${text}`} onClick={remove}>81              <svg aria-hidden="true" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round">82                <path d="M18 6 6 18M6 6l12 12" />83              </svg>84            </button>85          )}86        </motion.span>87      )}88    </AnimatePresence>89  );90}