Submit button

Submits the form, and shows a spinner when that takes a while.

Installation

npx shadcn@latest add @formdef/submit-button

Register it as a form component in lib/formdef/index.tsx, as formComponents: { SubmitButton }, and place it with a layout entry.

Usage

declare const : (: string) => Promise<void>

export const  = createForm({
  : .({ : .('Enter an email address') }),
  : { : '' },
  fields: { email: { : 'text', : { : 'Email' } } },
  : ['email', { : 'SubmitButton', : { : 'Subscribe' } }],
  onSubmit: ({  }) => (.),
})

The button reads the form from the contexts in lib/formdef/context.ts, so it takes no form or field prop.

  • It is never disabled because the form is invalid. A click on an invalid form submits, and the errors appear at their fields.
  • While a submit runs it is aria-disabled and aria-busy, and a second click does nothing.
  • Once a submit has run for 300 ms, a spinner replaces its icon. A fast submit shows none.

variant, size and className go to shadcn's Button.

Props

Prop

Type

Source

'use client'

import { useStore } from '@tanstack/react-form'
import { LoaderCircleIcon } from 'lucide-react'
import type { ComponentProps, ReactNode } from 'react'

import { Button } from '@/components/ui/button'
import { useSlow } from '@/hooks/formdef/use-slow'
import { useFormContext } from '@/lib/formdef/context'

type SubmitButtonProps = Pick<ComponentProps<typeof Button>, 'variant' | 'size' | 'className'> & {
  label: string
  icon?: ReactNode
}

export const SubmitButton = ({ label, icon, ...attrs }: SubmitButtonProps) => {
  const form = useFormContext()
  const isSubmitting = useStore(form.store, (state) => state.isSubmitting)
  const isSlow = useSlow(isSubmitting)

  return (
    <Button
      {...attrs}
      type="submit"
      aria-busy={isSubmitting}
      aria-disabled={isSubmitting}
      onMouseDown={(event) => event.preventDefault()}
      onClick={(event) => {
        if (isSubmitting) {
          event.preventDefault()
        }
      }}
    >
      {isSlow ? (
        <LoaderCircleIcon
          data-slot="spinner"
          className="motion-safe:animate-spin motion-reduce:animate-pulse"
        />
      ) : (
        icon
      )}
      {label}
    </Button>
  )
}