-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
feat(stepper): new stepper component #318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
@damianricobelli is attempting to deploy a commit to the shadcn-pro Team on Vercel. A member of the Team first needs to authorize it. |
The latest updates on your projects. Learn more about Vercel for Git ↗︎
1 Skipped Deployment
|
love it 👀 |
This looks incredible @damianricobelli I'll review. |
Looks amazing 😍 Unfortunately I don't have time to review and research about this component. However, look what I recently found: https://saas-ui.dev/docs/components/navigation/stepper. This can serve as reference to improve or borrow ideas to simplify the implementation. From what I can suggest it is to rename Step to StepperStep and useSteps to useStepper to comply with the general API conventions of the components and it will be more unique name to prevent conflicts. |
Thank you very much for your feedback! I'll be reviewing tomorrow what you just shared and your suggestions 🫶 |
@shadcn What do you think about this component? Do you think we should adjust anything so that it can be launched on prod? |
Is this is still in progress? |
@destino92 From my side the component is ready. Just need to know if @shadcn agrees to move forward and add it to the CLI that brings and details that you think are missing in terms of documentation. |
This looks good! |
Hi @damianricobelli, this component looks very good. Could you please update it to the new version (different themes, registry, docs, etc.)? |
@dan5py yes of course. Between today and Monday I will be making the necessary changes so that the component allows the last addition you mention. |
@shadcn Could you check this? I've already updated the code with all the latest stuff in the main branch. There are already several people watching the release of this component 🤩 🚀 |
Done @dan5py! 🥳 |
Wow, @damianricobelli, that was fast! 🚀 I haven’t tested it yet, but it looks super straightforward. However, I noticed that in your example, you didn’t use the stepper component from this PR, right? It seems like you built a much simpler version—looks great! But I was also hoping to test the scenario where I install your component via shadcn-cli and integrate it as expected from this PR.
You mentioned that the query params approach is much simpler. Do you also have an example showcasing this approach? The main issue I wanted to tackle with route-based steps is that when a user hits the back button, it shouldn’t take them to the last visited page but rather to the previous step. I initially tried solving this with shallow routing, but it turned out to be overly complex—at least in my implementation. Maybe you have a cleaner, more straightforward solution in mind? Looking forward to your thoughts! 😊 |
@iamfj Actually use the library on which the component of this PR is based for simplicity. You can achieve the same with the component of this PR without problems. As for the query params example, I can create an example in the next few days. And regarding modifying the back button to respond to the steps: you should simply have a custom button that uses the back method of the useStepper of your instance. |
@damianricobelli Got it! I wasn’t referring to the stepper’s back button but rather the browser’s back button. When a user clicks it, I’d like it to navigate to the previous step instead of the last visited page. |
🚀 |
@damianricobelli can you please use the new shadcn registry to host this somewhere (maybe on github) and give a link, so that we can |
when can we expect this stepper feature to be merged in shadcn ui ??? @shadcn |
I'll get back to new components right after the Tailwind v4 work. |
Now that we have tailwind v4, can we merge this please? |
# Conflicts: # apps/www/package.json # pnpm-lock.yaml
@damianricobelli I'm back reviewing this. Trying to see if we can simplify the API a bit and make it more composable. Questions: Do I was wondering if we can have: import { StepperProvider, StepperNavigation, StepperStep } from "@/components/ui/stepper"
export function Example() {
const { methods, steps, ...props } = useStepper({
steps: [...]
})
return (
<StepperProvider {...props}>
<StepperNavigation>
{steps.map((step) => (
<StepperStep key={step.id} of={step.id} onClick={() => methods.goTo(step.id)}>
</StepperStep>
))}
</StepperProvider>
)
} |
@shadcn yes sir. They needs to come from defineStepper to keep the API typesafe. Unless you come up with another idea, I think it would be difficult to keep the types If we don't do it this way. The API is inspired a little by how the typesafe handles the Tanstack Form hook, where you get a |
@shadcn I have been reviewing your question again and I don't think there is a way to separate the components of defineStepper. The reason is related to my previous comment: for the API to remain typesafe, we need to build these components based on the generics that traverse the defineStepper API |
@shadcn if you have any more questions or would like me to work on something in particular about this PR, don't hesitate to send me a DM on X/Twitter 🤝 |
I know it's long, but putting this here in case it helps. I created a wrapper around the API with React Context in order to make my dynamic form stepper more composable. Dashboard Component: 'use client';
export function FormPresenterDashboard({
formTitle = 'Form',
formSteps,
}: { formTitle?: string; formSteps: FormStepperStep[] }) {
const formStepper = defineStepper<FormStepperStep[]>(...formSteps);
return (
<FormStepper {...formStepper}>
<AppSidebar
header={
<div className="flex items-end gap-2 p-2">
<OrigamiIcon /> <span>Mentore</span>
</div>
}
content={
<SidebarMenu className="flex h-full flex-col gap-4 px-4 py-8">
<FormStepperNavigation />
<SidebarMenuItem className="mt-auto">
<SidebarMenuButton asChild>
<Link href="/dashboard">
<ArrowLeftIcon />
Back to Dashboard
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
}
>
<Header page={formTitle} />
<div className="mx-auto flex h-full w-full max-w-4xl flex-col gap-8 p-6 pb-24">
<FormStepperContent />
</div>
</AppSidebar>
</FormStepper>
);
} Stepper Wrapper type FormStepperStep = FormStepSchema;
type FormStepperType = DefineStepperProps<FormStepperStep[]>;
type FormStepperContextValue = FormStepperType;
const FormStepperContext = React.createContext<FormStepperContextValue | null>(
null
);
const useFormStepper = () => {
const onboardingStepperContext = React.useContext(FormStepperContext);
if (!onboardingStepperContext) {
throw new Error('useFormStepper must be used within a <FormStepper>');
}
return onboardingStepperContext;
};
function FormStepper({
children,
...stepper
}: { children: React.ReactNode } & FormStepperContextValue) {
const { StepperProvider } = stepper;
return (
<FormStepperContext.Provider value={stepper}>
<StepperProvider className="space-y-4" variant="vertical">
{children}
</StepperProvider>
</FormStepperContext.Provider>
);
}
function FormStepperNavigation() {
const { StepperNavigation, StepperStep, StepperTitle, useStepper } =
useFormStepper();
const methods = useStepper();
return (
<StepperNavigation>
{methods.all.map((step) => (
<StepperStep key={step.id} of={step.id} className="pointer-events-none">
<StepperTitle>{step.title}</StepperTitle>
</StepperStep>
))}
</StepperNavigation>
);
}
function FormStepperContent() {
const { StepperPanel, useStepper } = useFormStepper();
const methods = useStepper();
return (
<div>
{methods.when(methods.current.id, (step) => (
<StepperPanel>
<Card>
<CardHeader>
<CardTitle>{step.title}</CardTitle>
<CardDescription>{step.description}</CardDescription>
</CardHeader>
<CardContent>
<FormStep {...step} />
</CardContent>
</Card>
</StepperPanel>
))}
</div>
);
}
function FormStepperControls() {
const { StepperControls, useStepper } = useFormStepper();
const methods = useStepper();
return (
<StepperControls className="mt-auto">
<Button
variant="secondary"
onClick={methods.prev}
disabled={methods.isFirst}
>
Previous
</Button>
<Button onClick={methods.next} disabled={methods.isLast}>
{'Next'}
</Button>
</StepperControls>
);
}
export {
FormStepper,
FormStepperNavigation,
FormStepperContent,
FormStepperControls,
type FormStepperStep,
type FormStepperType,
useFormStepper,
};
This allows for using stepper controls in my form steps. export function FormStep(formStep: FormStepSchema) {
const { useStepper } = useFormStepper();
const formStepper = useStepper();
const form = useForm({});
const formStore = useFormStore((store) => store);
async function onSubmit(data: Record<string, string>) {
await delay(200); // Simulate async
const payload = { ...data, ...formStore.data };
if (formStepper.isLast) {
console.log(payload); // Submit form
} else {
formStore.setData(payload);
formStepper.next(); // Form stepper method
}
}
const colSpan = {
1: 'col-span-1',
2: 'col-span-2',
3: 'col-span-3',
4: 'col-span-4',
};
return (
<FormStepContext.Provider value={form}>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid grid-cols-4 gap-6"
>
{formStep.fields.map((field) => (
<div className={cn(colSpan[field.colSpan ?? '4'])} key={field.id}>
<FormField {...field} />
</div>
))}
<div className="col-span-4 flex flex-col-reverse gap-3 lg:ml-auto lg:w-fit lg:flex-row">
{!formStepper.isFirst && (
<Button
onClick={formStepper.prev}
type="button"
variant={'secondary'}
>
Previous
</Button>
)}
<LoadingButton disabled={form.formState.isSubmitting} type="submit">
{formStep.buttonLabel}
</LoadingButton>
</div>
</form>
</Form>
</FormStepContext.Provider>
);
} |
Hi everyone, I will post here my implementation based on the amazing library from @damianricobelli. Thank you so much for your job! I love it! I definetly tried to reproduce your example in your page (https://stepperize.vercel.app/)
import { Step, Stepper, StepperReturn } from "@stepperize/react";
import { AnimatePresence, motion } from "framer-motion";
import { Button } from "./button";
import { CheckCircle, LucideIcon } from "lucide-react";
import clsx from "clsx";
import { Fragment, ReactNode } from "react";
export interface StepperStep extends Step {
id: string;
icon: LucideIcon;
title: string;
// stepContent: ReactNode;
}
export type GetStepContentFunc = (
stepperApi: Stepper<StepperStep[]>
) => ReactNode;
type ScopedStepperProps = {
stepper: StepperReturn<StepperStep[]>;
};
type ScopedStepperContentProps = {
stepContents: Record<string, GetStepContentFunc>;
} & ScopedStepperProps;
export const ScopedStepper: React.FC<ScopedStepperContentProps> = ({
stepper,
stepContents,
}) => {
return (
<div className="backdrop-blur-sm border rounded-xl overflow-hidden shadow-xl w-full">
<stepper.Scoped>
<StepperSteps stepper={stepper} />
<div className="p-8">
<ScopedStepContent stepper={stepper} stepContents={stepContents} />
<StepNavigation stepper={stepper} />
</div>
</stepper.Scoped>
</div>
);
};
function StepperSteps({ stepper }: ScopedStepperProps) {
const { utils, useStepper } = stepper;
const _stepper = useStepper();
const currentIndex = utils.getIndex(_stepper.current.id);
return (
<nav className="bg-slate-800/80 p-8">
<ol className="flex justify-between items-center relative">
<div className="absolute top-5 left-7 xs:left-12 right-7 h-0.5 bg-slate-900 z-0 w-[93%]">
<motion.div
initial={{ width: 0 }}
animate={{
width: `${(currentIndex / (utils.getAll().length - 1)) * 100}%`,
}}
transition={{ duration: 0.4 }}
className="h-full bg-primary"
></motion.div>
</div>
{_stepper.all.map((step, index) => {
const isCompleted = index < currentIndex;
const isActive = index === currentIndex;
return (
<motion.div
key={step.id}
initial={false}
animate={{ scale: isActive ? 1.1 : 1 }}
transition={{ type: "spring", stiffness: 300 }}
className="flex flex-col items-center relative flex-shrink-0 z-10 transform-none"
>
<div
className={clsx(
"size-10 rounded-full flex items-center justify-center cursor-pointer",
"transform-none",
isCompleted || isActive
? "bg-primary border-none text-slate-300 dark:text-slate-800"
: "border-slate-300 text-slate-300 bg-white dark:bg-slate-800"
// "w-8 h-8 flex items-center justify-center rounded-full border-2 transition-colors duration-300 select-none"
)}
>
<div className="transform-none">
{isCompleted ? (
<CheckCircle className="w-4 h-4" />
) : (
<step.icon size={20} />
)}
</div>
</div>
<span
className={clsx(
"text-xs mt-2 hidden sm:block",
isCompleted || isActive
? " text-primary/80"
: " text-slate-800 dark:text-slate-300"
)}
>
{step.title}
</span>
</motion.div>
);
})}
</ol>
</nav>
);
}
export function ScopedStepContent({
stepper,
stepContents,
}: ScopedStepperContentProps) {
const _stepper = stepper.useStepper();
return (
<Fragment>
<AnimatePresence mode="wait">
<motion.div
initial={{ opacity: 0, x: 40 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -40 }}
transition={{ duration: 0.4 }}
className="w-full"
>
<h3 className="text-xl font-semibold mb-6 text-gray-12">
{_stepper.current.title}
</h3>
{stepContents[_stepper.current.id](_stepper)}
</motion.div>
</AnimatePresence>
</Fragment>
);
}
function StepNavigation({ stepper }: ScopedStepperProps) {
const _stepper = stepper.useStepper();
return (
<div className="mt-8 flex justify-between">
{!_stepper.isFirst && (
<Button onClick={_stepper.prev} type="button">
Indietro
</Button>
)}
<Button
onClick={_stepper.isLast ? undefined : _stepper.next}
className="ml-auto"
>
{_stepper.isLast ? "Conferma" : "Avanti"}
</Button>
</div>
);
} Usage "use client";
import {
GetStepContentFunc,
ScopedStepper,
StepperStep,
} from "@/components/ui/stepper";
import { defineStepper, Stepper } from "@stepperize/react";
import { User, Tag } from "lucide-react";
import { Button } from "@/components/ui/button";
export default function RegistrationStepper() {
const steps: StepperStep[] = [
{
id: "first",
title: "This is the first step",
icon: User,
},
{
id: "second",
title: "This is the second step",
icon: Tag,
},
{
id: "third",
title: "This is the third step",
icon: User,
},
{
id: "last",
title: "This is the last step",
icon: Tag,
},
];
const stepper = defineStepper(...steps);
const stepperContents: Record<string, GetStepContentFunc> = {
first: (stepper: Stepper<StepperStep[]>) => (
<>
<p>First step: {stepper.current.title}</p>
<Button onClick={stepper.next}>Test</Button>
</>
),
second: (stepper: Stepper<StepperStep[]>) => (
<p>Second step: {stepper.current.title}</p>
),
third: (stepper: Stepper<StepperStep[]>) => (
<>
<p>Third step: {stepper.current.title}</p>
<Button onClick={stepper.prev}>Test</Button>
</>
),
last: (stepper: Stepper<StepperStep[]>) => (
<p>Third step: {stepper.current.title}</p>
),
};
return (
<div className="p-4 flex justify-center">
<ScopedStepper stepper={stepper} stepContents={stepperContents} />
</div>
);
} If you prefer, you can move your content directly to your steps: {
id: "first",
title: "This is the first step",
icon: User,
content: (stepper: Stepper<StepperStep[]>) => (
<>
<p>First step: {stepper.current.title}</p>
<Button onClick={stepper.next}>Test</Button>
</>
)
}, then in your call in the {_stepper.current.content(_stepper)} |
@shadcn when will this PR get reviewed and merged in the main branch as stepper is an important component which is needed plenty of applications |
Hey @rhakbari, I understand your frustration, but we must understand that @shadcn is probably working on other things or simply does not have the time necessary to thoroughly review this PR. If you still need the component, you are free to take it from the PR and use it in your apps as many others have already done. And if you encounter any problems, you can simply create an issue in the stepperize repo or comment here |
With all due respect @damianricobelli, I don't think this will ever be merged into shadcn-ui. You released this as a separate library and the whole idea behind shadcn-ui is that whatever it is included are just very basic UI elements. It already provides a way to install other components from different registries, so the stepper you so carefully designed can already be used and installed independently. I wouldn't think merging the stepper as it is makes sense. Maybe @shadcn can bring some light here about the future of this PR. |
@leog thank you very much for your comment. In fact, we have already been in contact and in the next few days there will be a release about this through shadcn cli. Regarding your comment that shadcn only includes simple components, this is actually not so true since shadcn also includes tanstack table or otp input abstractions. So actually stepperize has all the possibilities to be part of shadcn, but in the end we will go another way after my conversation with @shadcn |
I am very happy to introduce ✨ Shadcn Stepper ✨. An abstraction of my stepperize library to create step-by-step flows with typesafe API integrated with shadcn cli and v0. https://x.com/damianricobelli/status/1914344064641626227 @shadcn Let's see how this abstraction goes 🙌 |
@damianricobelli thanks for the clarification and the update, I guess this PR can be closed then 👍 |
Hi! In this opportunity I present a new component: Stepper.
The idea of this in its beginnings was to make it as modular and flexible as possible for development.
A basic example of the application is this:
Here is a complete video of the different use cases:
Grabacion.de.pantalla.2023-05-08.a.la.s.13.14.45.mov