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
Customize
9 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add badgeWrites 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.
1import { Badge } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <Badge />6 );7}Props
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
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}