shadimage
FormCn
componentmulti-step-viewersource: ok · hit

Multi-step form

Multi-step form

npx shadcn add "https://formcn.dev/r/multi-step-viewer.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationmotion

Registry dependencies

@formcn/use-multi-step-viewer@formcn/stepperbutton

Files1 · source cached 10/8/2026, 8:16:23 PM

src/components/multi-step-viewer.tsx
'use client'
import type { VariantProps } from 'class-variance-authority'
import { AnimatePresence, type MotionProps, motion } from 'motion/react'
import * as React from 'react'
import { Button, type buttonVariants } from '@/components/ui/button'
import {
	Stepper,
	StepperIndicator,
	StepperItem,
	StepperSeparator,
	StepperTrigger,
} from '@/components/ui/stepper'
import { useMultiStepForm } from '@/hooks/use-multi-step-viewer'

const NextButton = (
	props: React.ComponentProps<'button'> &
		VariantProps<typeof buttonVariants> & {
			asChild?: boolean
		},
) => {
	const { isLastStep, goToNext } = useMultiStepForm()
	if (isLastStep) return null
	return (
		<Button size="sm" type="button" onClick={() => goToNext()} {...props} />
	)
}

const PreviousButton = (
	props: React.ComponentProps<'button'> &
		VariantProps<typeof buttonVariants> & {
			asChild?: boolean
		},
) => {
	const { isFirstStep, goToPrevious } = useMultiStepForm()
	if (isFirstStep) return null
	return (
		<Button
			size="sm"
			type="button"
			variant="outline"
			onClick={() => goToPrevious()}
			{...props}
		/>
	)
}

const SubmitButton = (
	props: React.ComponentProps<'button'> &
		VariantProps<typeof buttonVariants> & {
			asChild?: boolean
		},
) => {
	const { isLastStep } = useMultiStepForm()
	if (!isLastStep) return null
	return <Button size="sm" type="button" {...props} />
}

const ResetButton = (
	props: React.ComponentProps<'button'> &
		VariantProps<typeof buttonVariants> & {
			asChild?: boolean
		},
) => {
	return <Button size="sm" type="button" variant="ghost" {...props} />
}

const FormHeader = (props: React.ComponentProps<'div'>) => {
	const { currentStepIndex, steps } = useMultiStepForm()
	return (
		<div
			className="flex flex-col items-start justify-center gap-1 pb-4"
			{...props}
		>
			<Stepper value={currentStepIndex} orientation="horizontal">
				{steps.map((_, index) => {
					const stepNumber = index + 1
					const isLast = stepNumber === steps.length
					return (
						<StepperItem
							key={stepNumber}
							step={stepNumber}
							className="not-last:flex-1"
						>
							<StepperTrigger>
								<StepperIndicator />
							</StepperTrigger>
							{!isLast && <StepperSeparator />}
						</StepperItem>
					)
				})}
			</Stepper>
		</div>
	)
}
const FormFooter = (props: React.ComponentProps<'div'>) => {
	return (
		<div
			className="w-full pt-3 flex items-center justify-end gap-3"
			{...props}
		/>
	)
}

const StepFields = (props: React.ComponentProps<'div'> & MotionProps) => {
	const { currentStepIndex, steps } = useMultiStepForm()
	const currentFormStep = steps[currentStepIndex - 1]
	if (
		!currentFormStep ||
		currentStepIndex < 1 ||
		currentStepIndex > steps.length
	) {
		return null
	}
	return (
		<AnimatePresence mode="popLayout">
			<motion.div
				key={currentStepIndex}
				initial={{ opacity: 0, x: 15 }}
				animate={{ opacity: 1, x: 0 }}
				exit={{ opacity: 0, x: -15 }}
				transition={{ duration: 0.4, type: 'spring' }}
				{...props}
				className="grid grid-cols-6 gap-4"
			>
				{currentFormStep.component}
			</motion.div>
		</AnimatePresence>
	)
}

function MultiStepFormContent(props: React.ComponentProps<'div'>) {
	return <div className="flex flex-col gap-8 pt-3" {...props} />
}

export {
	MultiStepFormContent,
	FormHeader,
	FormFooter,
	StepFields,
	// Form Actions
	NextButton,
	PreviousButton,
	SubmitButton,
	ResetButton,
}

More from FormCn

Browse all components in this library →