Submit Button

Form submission button that automatically manages loading and disabled states based on form validation and submission status.

npx shadcn@latest add https://shuip.plvo.dev/r/tsf-submit-button.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/tsf-submit-button.json
bun x shadcn@latest add https://shuip.plvo.dev/r/tsf-submit-button.json
import type { FormAsyncValidateOrFn, FormValidateOrFn, ReactFormApi } from '@tanstack/react-form';
import type { VariantProps } from 'class-variance-authority';
import { Loader2Icon } from 'lucide-react';
import { Button, type buttonVariants } from '@/components/ui/button';
type ButtonProps = React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
};
export interface SubmitButtonProps<TFormData> {
form: ReactFormApi<
TFormData,
undefined | FormValidateOrFn<TFormData>,
undefined | FormValidateOrFn<TFormData>,
undefined | FormAsyncValidateOrFn<TFormData>,
undefined | FormValidateOrFn<TFormData>,
undefined | FormAsyncValidateOrFn<TFormData>,
undefined | FormValidateOrFn<TFormData>,
undefined | FormAsyncValidateOrFn<TFormData>,
undefined | FormValidateOrFn<TFormData>,
undefined | FormAsyncValidateOrFn<TFormData>,
undefined | FormAsyncValidateOrFn<TFormData>,
any
>;
children?: React.ReactNode;
props?: ButtonProps;
}
export function SubmitButton<TFormData>({ form, children = 'Submit', props }: SubmitButtonProps<TFormData>) {
return (
<form.Subscribe selector={(state) => [state.isSubmitting, state.canSubmit]}>
{([isSubmitting, canSubmit]) => (
<Button type='submit' disabled={isSubmitting || !canSubmit} className='transition-all duration-300' {...props}>
{isSubmitting && <Loader2Icon role='status' aria-label='Loading' className={'size-4 animate-spin'} />}
{children}
</Button>
)}
</form.Subscribe>
);
}
Loading...

Submit buttons need to prevent double submissions and show loading feedback. SubmitButton subscribes to the form's state via form.Subscribe to track isSubmitting and canSubmit, automatically disabling the button when the form is invalid or already submitting.

The component displays a Loader2Icon spinner when isSubmitting is true, providing visual feedback without manual state management. It accepts a props parameter for Button variants (outline, destructive, etc.) and all native button attributes.

Built-in features

  • Auto-disabled when !canSubmit or isSubmitting
  • Loading spinner via Loader2Icon during submission
  • Form subscription tracks state with zero boilerplate
  • Button variants via props.variant and props.size

Usage with validation

const form = useForm({
    defaultValues: { email: '' },
    onSubmit: async ({ value }) => {
      await saveData(value)
    }
})

<InputField
    form={form}
    name='email'
    label='Email'
    formProps={{
      validators: {
        onChange: ({ value }) => !value.includes('@') ? 'Invalid' : undefined
      }
    }}
/>

{/* Button disabled until email is valid */}
<SubmitButton form={form}>Submit</SubmitButton>

Custom variants

<SubmitButton form={form} props={{ variant: 'outline' }}>
    Save Draft
</SubmitButton>

<SubmitButton form={form} props={{ variant: 'destructive', size: 'lg' }}>
    Delete Account
</SubmitButton>

Examples

Default

Loading...
'use client';
import { useForm } from '@tanstack/react-form';
import { InputField } from '@/components/ui/shuip/tanstack-form/input-field';
import { SubmitButton } from '@/components/ui/shuip/tanstack-form/submit-button';
export default function TsfSubmitButtonExample() {
const form = useForm({
defaultValues: {
email: '',
},
onSubmit: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 2000));
alert(`Subscribed: ${value.email}`);
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
className='space-y-4 max-w-md'
>
<InputField
form={form}
name='email'
label='Email'
description='Subscribe to our newsletter'
formProps={{
validators: {
onChange: ({ value }) =>
!value ? 'Email is required' : !value.includes('@') ? 'Invalid email' : undefined,
},
}}
props={{ type: 'email', placeholder: 'Email' }}
/>
<SubmitButton form={form}>Subscribe</SubmitButton>
</form>
);
}

Props

Prop

Type

On this page