Objective Bar
A bar that displays the current objective
Made by lucasOnly 6 steps left
import ObjectiveBar from "@/components/targetblank/components/objective-bar";
import { CheckIcon, LucideIcon, TruckIcon } from "lucide-react";
import { motion } from "motion/react";
import * as React from "react";
const STEPS = 6;
export const ObjectiveBarDemo = () => {
const [currentStep, setCurrentStep] = React.useState<number>(0);
const [isFinished, setIsFinished] = React.useState<boolean>(false);
const [icon, setIcon] = React.useState<LucideIcon | undefined>(undefined);
React.useEffect(() => {
switch (currentStep) {
case 0:
setIcon(CheckIcon);
break;
case 3:
case 5:
setIcon(TruckIcon);
break;
default:
setIcon(undefined);
}
const interval = setInterval(() => {
setCurrentStep((prev) => (STEPS - prev === 1 ? 0 : prev + 1));
if (STEPS - currentStep === 1) {
setIsFinished(true);
} else {
setIsFinished(false);
}
}, 1000);
return () => clearInterval(interval);
}, [currentStep]);
return (
<ObjectiveBar steps={STEPS} currentStep={currentStep} icon={icon}>
<div className="flex flex-col gap-2 text-center mt-2">
{isFinished ? (
<motion.span
key="finished"
className="text-sm"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
>
All done! 🎉
</motion.span>
) : (
<motion.span
key="not-finished"
className="text-sm"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
>
{`Only ${STEPS - currentStep} ${
STEPS - currentStep === 1 ? "step" : "steps"
} left`}
</motion.span>
)}
</div>
</ObjectiveBar>
);
};
Installation
Install the following dependencies:
Copy and paste the following code into your project:
import { cn } from "@/lib/utils";
import { PackageIcon } from "lucide-react";
import { motion } from "motion/react";
import * as React from "react";
interface ObjectiveBarProps {
startLabel?: string;
endLabel?: string;
bgColor?: string;
accentColor?: string;
icon?: React.ElementType;
steps?: number;
currentStep?: number;
children: React.ReactNode;
}
const ObjectiveBar = ({
startLabel,
endLabel,
bgColor,
accentColor,
icon: Icon = PackageIcon,
steps = 4,
currentStep: currentStepProp = 2,
children,
}: ObjectiveBarProps) => {
const [currentStep, setCurrentStep] = React.useState<number>(0);
const currentStepRef = React.useRef<HTMLDivElement>(null);
const points = Array.from({ length: steps }, (_, i) => i);
const primaryColor = React.useMemo(() => bgColor || "#000", [bgColor]);
const secondaryColor = React.useMemo(
() => accentColor || "#fff",
[accentColor],
);
React.useEffect(() => {
setTimeout(() => {
setCurrentStep(currentStepProp);
}, 1000);
}, [currentStepProp]);
return (
<div className="flex flex-col gap-2 w-full rounded-lg p-2">
<div
className="relative w-full h-6 rounded-full border"
style={{
background: primaryColor,
}}
>
{startLabel && (
<span
className="absolute left-2 top-1/2 -translate-y-1/2 text-muted text-xs z-20"
style={{
color: currentStep === 0 ? secondaryColor : primaryColor,
}}
>
{startLabel}
</span>
)}
<motion.div
className="absolute h-full rounded-l-full"
style={{
background: secondaryColor,
}}
animate={{ width: `calc(${(currentStep / (steps - 1)) * 100}%)` }}
transition={{ type: "spring", stiffness: 120, damping: 20 }}
/>
<motion.div
ref={currentStepRef}
className={cn(
"absolute top-1/2 -translate-y-1/2 shadow-sm flex items-center justify-center rounded-full p-2 z-30 border",
currentStep !== 0 && "-translate-x-1/2",
)}
style={{
background: secondaryColor,
}}
animate={{
left: `calc(${(currentStep / (steps - 1)) * 100}% - 2px)`,
}}
transition={{ type: "spring", stiffness: 120, damping: 20 }}
>
<Icon className="size-6" style={{ color: primaryColor }} />
</motion.div>
{endLabel && (
<span
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted text-xs z-20"
style={{
color: currentStep !== steps ? secondaryColor : primaryColor,
}}
>
{endLabel}
</span>
)}
{points.length > 2 &&
points.slice(1, -1).map((point, index) => (
<span
key={point}
className="absolute top-1/2 -translate-y-1/2 z-10 rounded-full size-1 transition-all duration-300"
style={{
left: `calc(${(point / (steps - 1)) * 100}% - 2px)`,
background: index < currentStep ? primaryColor : secondaryColor,
}}
/>
))}
</div>
{children}
</div>
);
};
export default ObjectiveBar;
Update the import paths to match your project setup.
Usage
<ObjectiveBar />
Props
Prop | Type | Default |
---|---|---|
children | React.ReactNode | - |
currentStep? | number | - |
steps? | number | - |
icon? | React.ElementType | PackageIcon |
accentColor? | string | - |
bgColor? | string | - |
endLabel? | string | - |
startLabel? | string | - |
Credits
- Credits to @MarkKnd for the inspiration.