Multi-step form

PreviousNext

Multi-step form

Docs
formcncomponent

Preview

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

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, goToNext } = 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">
      {/* @ts-expect-error caused when upgrading to Nextjs 16 */}
      <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,
};

Installation

npx shadcn@latest add @formcn/multi-step-viewer

Usage

import { MultiStepViewer } from "@/components/multi-step-viewer"
<MultiStepViewer />