Created
February 22, 2024 20:19
-
-
Save adevinwild/27fe1063e4a17d680513005ad2b8b97a to your computer and use it in GitHub Desktop.
A simple Stepper made with TailwindCSS, you'll need `tailwind-merge` and `clsx` (`cn` function we can found for example inside `shadcn/ui`)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use client"; | |
import React, { | |
Fragment, | |
createContext, | |
useContext, | |
type HTMLAttributes, | |
type ReactNode, | |
} from "react"; | |
import { cn } from "../../utils/cn"; | |
type StepperProps = { | |
children?: ReactNode; | |
value?: number; | |
onValueChange?: (_v: number) => void; | |
steps?: number; | |
} & HTMLAttributes<HTMLDivElement>; | |
type StepperContext = { | |
currentStep: number; | |
setCurrentStep: (_v: number) => void; | |
}; | |
const StepperContext = createContext<StepperContext | null>(null); | |
export const Stepper = ({ | |
className, | |
children, | |
steps, | |
...props | |
}: StepperProps) => { | |
return ( | |
<StepperContext.Provider | |
value={{ | |
currentStep: props.value ?? 0, | |
setCurrentStep: props.onValueChange ?? (() => {}), | |
}} | |
> | |
<div | |
className={cn("flex w-full items-center justify-between", className)} | |
{...props} | |
> | |
{steps && | |
steps > 0 && | |
Array.from({ length: steps }).map((_, index) => { | |
if (index < steps - 1) { | |
const isActiveOrPassed = index < (props.value ?? 0); | |
return ( | |
<Fragment key={index}> | |
<Step index={index} /> | |
<div | |
className={cn( | |
"z-0 w-full border-b border-dashed transition-all duration-300 ease-in", | |
isActiveOrPassed && "border-solid border-blue-500" | |
)} | |
/> | |
</Fragment> | |
); | |
} | |
return <Step key={index} index={index} />; | |
})} | |
{children && | |
React.Children.map(children, (child, index) => { | |
if (!React.isValidElement(child)) return null; | |
const clonedChild = React.cloneElement(child, { | |
index, | |
} as StepProps); | |
const length = React.Children.count(children); | |
// We want to add divider between steps | |
if (index < length - 1) { | |
const isActiveOrPassed = index < (props.value ?? 0); | |
return ( | |
<> | |
{clonedChild} | |
<div | |
className={cn( | |
"z-0 w-full border-b border-dashed transition-all duration-300 ease-in", | |
isActiveOrPassed && "border-solid border-blue-500" | |
)} | |
/> | |
</> | |
); | |
} | |
return clonedChild; | |
})} | |
</div> | |
</StepperContext.Provider> | |
); | |
}; | |
type StepProps = { | |
children?: ReactNode; | |
onClick?: () => void; | |
index?: number; | |
} & HTMLAttributes<HTMLDivElement>; | |
export const Step = ({ children, className, index, ...props }: StepProps) => { | |
const { currentStep } = useStepper(); | |
if (typeof index !== "number" && typeof index !== "string") { | |
return null; | |
} | |
const isActive = index === currentStep || index < currentStep; | |
return ( | |
<div | |
className={cn( | |
"z-10 flex aspect-square size-6 items-center justify-center rounded-full bg-slate-100 text-xs font-semibold text-slate-500 ring-1 ring-slate-200 transition-all duration-300 lg:size-8 lg:text-sm", | |
isActive && "bg-blue-500 text-white ring-blue-500", | |
className | |
)} | |
{...props} | |
> | |
{children ?? index + 1} | |
</div> | |
); | |
}; | |
function useStepper() { | |
const context = useContext(StepperContext); | |
if (!context) { | |
throw new Error("useStepper must be used within a Stepper component"); | |
} | |
return context; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment