bjork-ui
componentanimated-status-badgesource: ok · hit
Animated status badge
Animated status badge is a production-ready badges component from BJORK UI, packaged for shadcn registries.
npx shadcn add "https://ui.isaiahbjork.com/animated-status-badge.json"free
Where it goes
stats
Interaction
animated-counter
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumanimationframer-motion
iconslucide-react
Files3 · source cached 10/9/2026, 10:17:17 PM
components/bjork-ui/badges/animated-status-badge.tsx
"use client"
import { useState, useEffect } from "react"
import { AnimatePresence, useReducedMotion } from "framer-motion"
import { Loader2, Check } from "lucide-react"
import { BjorkBadge } from "@/components/bjork-ui/primitives/badge"
import { cn } from "@/lib/utils"
interface AnimatedStatusBadgeProps {
trigger: boolean
onAnimationComplete?: () => void
className?: string
}
export function AnimatedStatusBadge({
trigger,
onAnimationComplete,
className = ""
}: AnimatedStatusBadgeProps) {
const [isAnimating, setIsAnimating] = useState(false)
const [isCompleted, setIsCompleted] = useState(false)
const reduce = useReducedMotion()
const startAnimation = () => {
setIsAnimating(true)
setIsCompleted(false)
setTimeout(() => {
setIsAnimating(false)
setTimeout(() => {
setIsCompleted(true)
// Make completed badge disappear after 3 seconds
setTimeout(() => {
setIsCompleted(false)
if (onAnimationComplete) {
onAnimationComplete()
}
}, 3000)
}, 300) // Delay the appearance of "Completed" badge
}, 3000) // Animation duration
}
useEffect(() => {
if (!isAnimating && !isCompleted) {
setIsCompleted(false)
}
}, [isAnimating, isCompleted])
useEffect(() => {
if (trigger) {
startAnimation()
}
}, [trigger])
return (
<>
<AnimatePresence>
{isAnimating && (
<BjorkBadge
variant="outline"
className={cn(
"absolute right-0 top-0 z-0 border-[#ec5c13]/28 bg-[#ec5c13]/12 text-xs font-medium text-[#bd4514] shadow-[0_12px_18px_-14px_rgba(236,92,19,0.5)] dark:text-[#d86a2c]",
className
)}
initial={reduce ? false : { y: 40, opacity: 1 }}
animate={{ y: -32, opacity: 1 }}
exit={{
y: [-37, 40], // First go up 5px, then slide down
opacity: [1, 1, 0], // Maintain opacity until the end of the animation
scale: [1, 0.8, 0.8], // Scale down as it starts to disappear
}}
transition={{
duration: reduce ? 0 : 0.5,
times: [0, 0.2, 1], // Timing for the exit animation stages
ease: "easeInOut",
}}
>
<Loader2 className={cn("h-3 w-3 mr-1", !reduce && "animate-spin")} />
<span>Running</span>
</BjorkBadge>
)}
</AnimatePresence>
<AnimatePresence>
{isCompleted && (
<BjorkBadge
variant="accent"
className={cn(
"absolute right-0 top-0 z-0 text-xs font-medium shadow-[0_12px_18px_-14px_rgba(236,92,19,0.5)]",
className
)}
initial={reduce ? false : { y: 40, opacity: 1 }}
animate={{ y: -32, opacity: 1 }}
exit={{
y: [-37, 40], // First go up 5px, then slide down
opacity: [1, 1, 0], // Maintain opacity until the end of the animation
scale: [1, 0.8, 0.8], // Scale down as it starts to disappear
}}
transition={{
duration: reduce ? 0 : 0.5,
times: [0, 0.2, 1], // Timing for the exit animation stages
ease: "easeInOut",
}}
>
<Check className="h-3 w-3 mr-1" />
<span>Completed</span>
</BjorkBadge>
)}
</AnimatePresence>
</>
)
}
components/bjork-ui/primitives/badge.tsx
"use client";
import * as React from "react";
import { motion, useReducedMotion, type HTMLMotionProps } from "framer-motion";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const bjorkBadgeVariants = cva(
"inline-flex w-fit shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-[9px] border font-medium outline-none transition-[background-color,border-color,color,box-shadow,opacity,transform] duration-200 focus-visible:ring-2 focus-visible:ring-[color:var(--bjork-accent-muted)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bjork-ring-offset)] [&_svg]:size-3",
{
variants: {
variant: {
default:
"border-transparent bg-[var(--bjork-text)] text-[color:var(--bjork-inverted-text)] shadow-[var(--bjork-shadow-soft)]",
secondary:
"border-transparent bg-[var(--bjork-surface-active)] text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-soft)]",
outline:
"border-[color:var(--bjork-border)] bg-[var(--bjork-field-inset)] text-[color:var(--bjork-text-medium)]",
muted:
"border-transparent bg-[var(--bjork-field-muted)] text-[color:var(--bjork-text-soft)]",
accent:
"border-[color:var(--bjork-accent-muted)] bg-[var(--bjork-accent-badge)] text-[color:var(--bjork-accent-badge-foreground)] shadow-[var(--bjork-shadow-soft)]",
},
size: {
sm: "px-1.5 py-0.5 text-[11px] has-[>svg:first-child]:pl-1 has-[>svg:last-child]:pr-1",
md: "px-2 py-1 text-xs has-[>svg:first-child]:pl-1.5 has-[>svg:last-child]:pr-1.5",
lg: "px-2.5 py-1.5 text-sm has-[>svg:first-child]:pl-2 has-[>svg:last-child]:pr-2",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
},
);
export interface BjorkBadgeProps
extends Omit<HTMLMotionProps<"span">, "ref">,
VariantProps<typeof bjorkBadgeVariants> {}
export function BjorkBadge({
className,
variant,
size,
...props
}: BjorkBadgeProps) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.span
data-slot="bjork-badge"
className={cn(bjorkBadgeVariants({ variant, size, className }))}
whileHover={shouldReduceMotion ? undefined : { y: -1, opacity: 0.92 }}
transition={{ type: "spring", stiffness: 420, damping: 30 }}
{...props}
/>
);
}
export { bjorkBadgeVariants };
lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}